🅴ExHentai
https://exhentai.org/
分享者: laizhiyu (6509)发布时间: 18小时前
增加添加收藏夹功能,搜索tag,订阅tag功能
{
"articleStyle": 3,
"cacheFirst": false,
"concurrentRate": "1\/800",
"customOrder": 0,
"enableJs": true,
"enabled": true,
"enabledCookieJar": false,
"header": "<js>\n(function () {\n \/\/ 动态头:代理 + 里站 Cookie 全部从面板变量注入(proxy 键由 AnalyzeUrl.getProxyClient 原生识别)\n var UA = 'Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/120.0.0.0 Safari\/537.36';\n \/\/ ★261109 v3:UA 统一由这里注入(AnalyzeUrl → WebView.settings.userAgentString 同源,\n \/\/ 官方版 ReadRssActivity.initLiveData 从 headerMap[UA] 取;E 版同字段)。\n \/\/ 补 Accept-Language \/ Sec-Fetch-* 让指纹更接近真实浏览器,降低被风控概率。\n var h = {\n 'User-Agent': UA,\n 'Accept': 'text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/avif,image\/webp,*\/*;q=0.8',\n 'Accept-Language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7',\n 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate',\n 'Sec-Fetch-Site': 'same-origin', 'Upgrade-Insecure-Requests': '1',\n 'Referer': 'https:\/\/exhentai.org\/'\n };\n function g(k) {\n var v = '';\n try { v = String(source.get(k)); } catch (e) { v = ''; }\n if (v === 'null' || v === 'undefined' || v === 'NaN') v = '';\n return v;\n }\n var px = g('ehProxy');\n if (px) { h.proxy = px; }\n \/\/ ★261114 v6.8:收藏夹\/订阅tag 开关默认关闭(名单缓存上线,降低敏感接口请求频率)。\n \/\/ 一次性迁移:老用户若存过 '1',这里重置为 '0'(有 ehFavReset68 标记不重复执行)。\n try {\n if (!g('ehFavReset68')) {\n source.put('ehFavPanel', '0');\n source.put('ehFavReset68', '1');\n source.put('ehFavCache', '');\n source.put('ehTagCache', '');\n }\n } catch (eR68) { }\n \/\/ ★261114 v6.9 一次性迁移:清掉直连功能时代残留的 ehHostsIp(IP池直连会被站方按共享IP限频)。\n \/\/ 新守卫 ehFix69:LT 机器上 ehFavReset68 已消费过,必须用新键才执行。\n try {\n if (!g('ehFix69')) {\n source.put('ehHostsIp', '');\n source.put('ehFix69', '1');\n }\n } catch (eF69) { }\n \/\/ ★★ 261114 v5.2:只发 ipb_member_id \/ ipb_pass_hash \/ inline_set \/ igneous(=空或该账号有效值),\n \/\/ 绝不发 yay \/ sk \/ star,绝不硬编码账号凭据。实测 igneous=mystery\/乱值 ⇒ 0B 白屏;\n \/\/ igneous= 空值 ⇒ 站方重签该账号有效值 + 62KB 正常。enabledCookieJar=false 截断回写;\n \/\/ 显式带 igneous 键是为覆盖 CookieStore 历史残留 mystery(mergeCookies putAll 缺键补齐)。\n var raw = g('ehCookie');\n \/\/ ★ 未配置用户 Cookie ⇒ 只发 igneous= 空值占位(防止 mergeCookies 把库内旧 Cookie 补回来)\n if (raw.indexOf('ipb_member_id=') < 0 || raw.indexOf('ipb_pass_hash=') < 0) {\n h.Cookie = 'igneous=';\n return JSON.stringify(h);\n }\n var parts = raw.split(';'), keep = [], seen = {}, i, p, eq, k, v;\n \/\/ ★261114 v5.1:必须显式带 igneous —— AnalyzeUrl.setCookie() 用 mergeCookiesToMap(putAll),\n \/\/ header 缺该键时 CookieStore 里残留的 igneous=mystery 会被补进请求 ⇒ 0B 白屏。\n var ALLOW = ['ipb_member_id', 'ipb_pass_hash', 'inline_set', 'igneous'];\n for (i = 0; i < parts.length; i++) {\n p = parts[i].trim(); eq = p.indexOf('=');\n if (eq < 1) continue;\n k = p.substring(0, eq).trim(); v = p.substring(eq + 1).trim();\n if (ALLOW.indexOf(k) < 0 || seen[k] || v === '') continue;\n seen[k] = 1; keep.push(k + '=' + v);\n }\n if (keep.join(';').indexOf('inline_set=') < 0) keep.push('inline_set=dm_l');\n \/\/ ★★261114 v5.2:igneous 是账号绑定值,绝不硬编码(v5.1 硬编码沙盒账号的值导致\n \/\/ 用户账号写操作全失败 \/ 静默用测试账号 → 收藏夹与关注列表是别人的)。\n \/\/ - 用户主动点过「🔄 刷新 igneous」→ 用 ehIgneous(该账号自己的有效值)\n \/\/ - 否则用【空值】:键存在可 putAll 覆盖 CookieStore 残留的 mystery,\n \/\/ 空值会让站方按「只发两项」重签该账号自己的 igneous(实测 62KB 正常)。\n var ig = g('ehIgneous') || '';\n if (ig === 'mystery' || ig === 'undefined' || ig === 'null' || ig === '0') ig = '';\n \/\/ 覆盖掉从粘贴串带进来的旧\/失效 igneous(统一用 ehIgneous 或空值)\n var cleaned = [];\n for (i = 0; i < keep.length; i++) if (keep[i].indexOf('igneous=') !== 0) cleaned.push(keep[i]);\n cleaned.push('igneous=' + ig);\n h.Cookie = cleaned.join('; ');\n return JSON.stringify(h);\n})()\n<\/js>",
"lastUpdateTime": 1794637096497,
"loadWithBaseUrl": true,
"loginCheckJs": "\/\/ ==================== loginCheckJs:绕开 OkHttp\/CookieJar 白屏(261107 定案) ====================\n\/\/ 源码 Rss.getArticles \/ Rss.getContentAwait:\n\/\/ analyzeUrl.getStrResponseAwait().let { analyzeUrl.evalJS(checkJs, it) as StrResponse }\n\/\/ bindings[\"result\"] = 原始 StrResponse;这里必须返回 StrResponse。\n\/\/ ★ 双栈对照实锤:OkHttp+CookieJar 栈对 exhentai HTML 恒 0B(igneous=mystery 白屏),\n\/\/ java.ajax(jsoup 纯净栈 + 只发 member\/pass 两项) 恒 60KB+ 正常 HTML。\n\/\/ ⇒ 空响应时用 ajax 重取,new StrResponse(url, fresh) 替换。\n\/\/ ★ Cookie 来源:仅面板变量 ehCookie;为空时不重取(明确提示用户粘贴),绝不硬编码测试账号。\n(function (r) {\n var UA = 'Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/120.0.0.0 Safari\/537.36';\n \/\/ ★261114 v5.2:绝不硬编码账号凭据\/igneous;没配 ehCookie 就保持原响应(由 header 层提示)\n function g(k) {\n var v = '';\n try { v = String(source.get(k)); } catch (e) { v = ''; }\n if (v === 'null' || v === 'undefined' || v === 'NaN' || v === '') v = '';\n return v;\n }\n \/\/ ★261114 v5.1:只发两项 + inline_set + 显式合法 igneous(覆盖 CookieStore 残留 mystery)\n function cookie2() {\n var raw = g('ehCookie');\n if (raw.indexOf('ipb_member_id=') < 0 || raw.indexOf('ipb_pass_hash=') < 0) return '';\n var parts = raw.split(';'), keep = [], seen = {}, OK = ['ipb_member_id', 'ipb_pass_hash', 'inline_set'];\n for (var i = 0; i < parts.length; i++) {\n var p = parts[i].trim(), eq = p.indexOf('=');\n if (eq < 1) continue;\n var k = p.substring(0, eq).trim(), v = p.substring(eq + 1).trim();\n if (OK.indexOf(k) < 0 || seen[k] || v === '') continue; \/\/ 忽略 igneous,下面统一补\n seen[k] = 1; keep.push(k + '=' + v);\n }\n if (keep.join(';').indexOf('inline_set=') < 0) keep.push('inline_set=dm_l');\n var ig = g('ehIgneous') || '';\n if (ig === 'mystery' || ig === 'undefined' || ig === 'null' || ig === '0') ig = '';\n keep.push('igneous=' + ig); \/\/ 空值占位:覆盖库内 mystery + 让站方重签该账号的值\n return keep.join('; ');\n }\n var url = '';\n try { url = String(r.url()); } catch (e) { return r; }\n var body = '';\n try { body = String(r.body()); } catch (e) { body = ''; }\n if (body === 'null' || body === 'undefined') body = '';\n \/\/ 已经正常就原样返回,避免每个请求都二次取\n if (body.length > 5000 && body.indexOf('<') === 0 && !\/temporarily banned|excessive request\/i.test(body.slice(0, 400))) return r;\n if (!url || url.indexOf('hentai.org') < 0) return r;\n var fresh = '';\n var CK2 = cookie2();\n if (!CK2) return r; \/\/ ★ 未配置用户 Cookie ⇒ 不重取(否则会用空 Cookie 打到 Sad Panda)\n try {\n var H = JSON.stringify({ 'User-Agent': UA, 'Cookie': CK2, 'Referer': 'https:\/\/exhentai.org\/' });\n \/\/ ★261114 v6.9 不再带 dnsIp(直连残留清除,对照实验实锤带IP池必吃banned页)\n var HO = JSON.parse(H);\n fresh = String(java.ajax(url + ',' + JSON.stringify(HO)));\n } catch (e) { fresh = ''; }\n if (fresh.length > 1000 && fresh.indexOf('<') === 0) {\n try {\n var nr = new Packages.io.legado.app.help.http.StrResponse(url, fresh);\n return nr;\n } catch (e2) { return r; }\n }\n return r;\n})(result)\n",
"loginUi": "[{\"name\": \"ehUser\", \"type\": \"text\", \"viewName\": \"'🔐 论坛账号'\"}, {\"name\": \"ehPass\", \"type\": \"password\", \"viewName\": \"'🔐 论坛密码'\"}, {\"name\": \"🔐 用账号密码登录\", \"type\": \"button\", \"action\": \"EH_pwdLogin()\", \"viewName\": \"EH_loginStatus()\"}, {\"name\": \"📝 注册新账号(浏览器)\", \"type\": \"button\", \"action\": \"EH_register()\"}, {\"name\": \"🔑 忘记密码 \/ 找回用户名\", \"type\": \"button\", \"action\": \"EH_forgot()\"}, {\"name\": \"🌐 打开论坛登录(过CF)\", \"type\": \"button\", \"action\": \"EH_browserLogin()\"}, {\"name\": \"🔄 读取登录 Cookie\", \"type\": \"button\", \"action\": \"EH_readBrowserCookie()\"}, {\"name\": \"✅ 读取Cookie并验证\", \"type\": \"button\", \"action\": \"EH_afterBrowser()\"}, {\"name\": \"━━━━━ 或粘贴 Cookie(现有通道) ━━━━━\", \"type\": \"button\", \"action\": \"\"}, {\"name\": \"ehCookieInput\", \"type\": \"text\", \"viewName\": \"'① 里站 Cookie'\"}, {\"name\": \"💾 保存 Cookie\", \"type\": \"button\", \"action\": \"EH_applyCookie()\"}, {\"name\": \"ehProxyInput\", \"type\": \"text\", \"viewName\": \"'② 代理地址'\"}, {\"name\": \"💾 保存代理\", \"type\": \"button\", \"action\": \"EH_applyProxy()\", \"viewName\": \"'💾 应用代理(留空=直连)'\"}, {\"name\": \"🩺 一键自愈(白屏必点)\", \"type\": \"button\", \"action\": \"EH_heal()\"}, {\"name\": \"🔄 刷新 igneous(登录态持久化)\", \"type\": \"button\", \"action\": \"EH_refreshIgneous()\", \"viewName\": \"EH_igBtn()\"}, {\"name\": \"🔍 检测连通 \/ 登录态\", \"type\": \"button\", \"action\": \"EH_check()\"}, {\"name\": \"🖼 正文图片布局\", \"type\": \"button\", \"action\": \"EH_toggleCols()\", \"viewName\": \"EH_colsBtn()\"}, {\"name\": \"📁 正文显示收藏夹\", \"type\": \"button\", \"action\": \"EH_favToggle()\", \"viewName\": \"EH_favBtn()\"}, {\"name\": \"🏷 点 tag 名字搜索对应标签\", \"type\": \"button\", \"action\": \"EH_tagToggle('ehTagSearch')\", \"viewName\": \"EH_tagBtn('ehTagSearch','点tag搜索')\"}, {\"name\": \"🏷 点 + 订阅对应 tag(再点取消)\", \"type\": \"button\", \"action\": \"EH_tagToggle('ehTagWatch')\", \"viewName\": \"EH_tagBtn('ehTagWatch','订阅tag')\"}, {\"name\": \"ehResSelect\", \"type\": \"select\", \"action\": \"EH_setRes()\", \"chars\": [\"省流·缩略图≈250px\", \"自动·均衡≈780x\", \"高清·1280x(WiFi)\", \"超清·全量·大屏\"], \"default\": \"自动·均衡≈780x\"}, {\"name\": \"⚙ 同步站点分辨率(xr\/ts)\", \"type\": \"button\", \"action\": \"EH_uconfigSync()\"}, {\"name\": \"📊 分辨率\/布局状态\", \"type\": \"button\", \"action\": \"EH_toast(EH_resStatus())\", \"viewName\": \"EH_resStatus()\"}, {\"name\": \"📊 当前配置\", \"type\": \"button\", \"action\": \"EH_toast(EH_status())\", \"viewName\": \"EH_status()\"}, {\"name\": \"✅ 检查登录状态\", \"type\": \"button\", \"action\": \"EH_checkLogin()\"}, {\"name\": \"🩺 登录态一键诊断\", \"type\": \"button\", \"action\": \"EH_diagLogin()\", \"viewName\": \"EH_diagBtn()\"}, {\"name\": \"━━━━━ 线路 \/ 测速 ━━━━━\", \"type\": \"button\", \"action\": \"\"}, {\"name\": \"⚡ 测试各线路延时\", \"type\": \"button\", \"action\": \"EH_testDelay()\"}, {\"name\": \"ehPubInput\", \"type\": \"text\", \"viewName\": \"'🌐 自定义线路'\"}, {\"name\": \"🔗 一键从发布页获取网址路线\", \"type\": \"button\", \"action\": \"EH_fromPub()\"}, {\"name\": \"里站 exhentai.org · 未测速(点击切换)\", \"type\": \"button\", \"action\": \"EH_switchHostEx(1)\", \"viewName\": \"EH_hostBtn(1)\"}, {\"name\": \"表站 e-hentai.org · 未测速(点击切换)\", \"type\": \"button\", \"action\": \"EH_switchHostEx(2)\", \"viewName\": \"EH_hostBtn(2)\"}, {\"name\": \"🚪 清除面板 Cookie\", \"type\": \"button\", \"action\": \"EH_clearCookie()\"}, {\"name\": \"🧹 清 App 域 Cookie\", \"type\": \"button\", \"action\": \"EH_purge()\"}]",
"loginUrl": "\/\/ ==================== ExHentai 订阅源 · 登录\/凭据\/代理面板 (v2) ====================\n\/\/ ★261107 修复要点(三组对照实验实锤):\n\/\/ ① V1 loginUi 的输入框值由 handleButtonClick 绑成 `result`(getLoginData 的 Map),\n\/\/ 不是 source.put ⇒ 旧版 action 读 source.get('ehCookieInput') 恒空(用户填了没保存)。\n\/\/ 现统一 EH_input(key) 从 result 读,兼容 login() 传参与 source.get 兜底。\n\/\/ ② 只发 ipb_member_id + ipb_pass_hash 两项:带旧 igneous \/ CookieJar merge 的\n\/\/ igneous=mystery ⇒ 站方 0B 白屏;不带则服务器重新签发有效 igneous(30天),HTML 正常。\n\/\/ ③ java.ajax 走纯净栈能拿 67KB HTML,java.connect(OkHttp+CookieJar) 恒 0B\n\/\/ ⇒ 检测与自愈一律用 ajax;订阅源正式请求靠 loginCheckJs 替换响应体(见 build.py)。\nvar EH_UA = 'Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/120.0.0.0 Safari\/537.36';\nvar EH_OK = ['ipb_member_id', 'ipb_pass_hash', 'inline_set'];\n\/\/ ★261114 v5.2:igneous 是账号绑定值,绝不硬编码;空值占位让站方重签该账号自己的值\n\nfunction EH_toast(t) {\n try { java.longToast(String(t)); } catch (e) { try { java.toast(String(t)); } catch (e2) {} }\n}\nfunction EH_gv(k, def) {\n var v = '';\n try { v = String(source.get(k)); } catch (e) { v = ''; }\n if (v === 'null' || v === 'undefined' || v === 'NaN' || v === '') v = def || '';\n return v;\n}\nfunction EH_set(k, v) { try { source.put(k, v); } catch (e) {} }\n\/\/ ★ 从 V1 面板绑定的 result(Map) 读输入框;三通道兜底\nfunction EH_input(k) {\n var v = '';\n try {\n if (typeof result !== 'undefined' && result != null) {\n if (typeof result.get === 'function') { try { v = String(result.get(k)); } catch (e0) {} }\n if (!v || v === 'null' || v === 'undefined') { try { v = String(result[k]); } catch (e1) {} }\n }\n } catch (e) {}\n if (!v || v === 'null' || v === 'undefined' || v === 'NaN') v = EH_gv(k, '');\n return v;\n}\nfunction EH_host() {\n var d = EH_gv('ehHost', 'https:\/\/exhentai.org').replace(\/\\\/+$\/, '');\n if (d.indexOf('http') !== 0) d = 'https:\/\/' + d;\n return d;\n}\n\/\/ 与 header.js 完全同逻辑:只保留两项 + inline_set,剔除 igneous\/yay\/sk\/star\nfunction EH_cookie(raw) {\n var parts = String(raw || '').split(';'), keep = [], seen = {}, i, p, eq, k, v;\n for (i = 0; i < parts.length; i++) {\n p = parts[i].trim(); eq = p.indexOf('=');\n if (eq < 1) continue;\n k = p.substring(0, eq).trim(); v = p.substring(eq + 1).trim();\n if (EH_OK.indexOf(k) < 0 || seen[k] || v === '') continue;\n seen[k] = 1; keep.push(k + '=' + v);\n }\n if (keep.join(';').indexOf('inline_set=') < 0) keep.push('inline_set=dm_l');\n var ig = EH_gv('ehIgneous', '') || '';\n if (ig === 'mystery' || ig === 'null' || ig === 'undefined' || ig === '0') ig = '';\n var cleaned = [];\n for (i = 0; i < keep.length; i++) if (keep[i].indexOf('igneous=') !== 0) cleaned.push(keep[i]);\n cleaned.push('igneous=' + ig);\n return cleaned.join('; ');\n}\n\/\/ 构造 java.ajax 的 URL 选项串(纯净栈,不装 CookieJar)\nfunction EH_ajaxUrl(path) {\n var h = { 'User-Agent': EH_UA, 'Cookie': EH_cookie(EH_gv('ehCookie', '')), 'Referer': 'https:\/\/exhentai.org\/' };\n return EH_host() + path + ',{\"headers\":' + JSON.stringify(h) + '}';\n}\n\n\/\/ ---- 💾 保存 Cookie(从 result 读粘贴框) ----\nfunction EH_applyCookie() {\n var raw = EH_input('ehCookieInput');\n if (!raw) { EH_toast('⚠ 请先在上方框里粘贴 Cookie,再点保存'); return; }\n var s = String(raw).replace(\/\\r\/g, '').replace(\/\\n\/g, ' ').trim();\n \/\/ 解析:接受 \"k: v\" 多行 \/ 分号串 \/ document.cookie\n var parts = s.replace(\/:\\s*\/g, '=').split(\/[;\\n]+\/), out = [], seen = {}, gotMember = false, gotPass = false;\n for (var i = 0; i < parts.length; i++) {\n var p = parts[i].trim(), eq = p.indexOf('=');\n if (eq < 1) continue;\n var k = p.substring(0, eq).trim(), v = p.substring(eq + 1).trim();\n if (seen[k] || v === '' || v === 'undefined' || v === 'null') continue;\n \/\/ ★261114 v5.1:粘贴串里的 igneous=mystery 绝不入库\n if (k === 'igneous') continue;\n if (k === 'ipb_member_id') gotMember = true;\n if (k === 'ipb_pass_hash') gotPass = true;\n seen[k] = 1; out.push(k + '=' + v);\n }\n if (!gotMember || !gotPass) {\n EH_set('ehCookie', s);\n EH_toast('⚠ 未同时识别到 ipb_member_id 与 ipb_pass_hash,已原样保存;请检查格式');\n } else {\n EH_set('ehCookie', out.join('; '));\n \/\/ ★ 关键:把 igneous 等容易失效的键从「发送串」里剔除(header 只发两项让站方重签)\n EH_set('ehCookieInput', out.join('; '));\n EH_toast('✅ 已保存(' + out.length + ' 项,已剔除 igneous 脏值)。★请求时用面板「🔄 刷新 igneous」维护的合法值,不再反复白屏');\n }\n EH_refresh();\n}\nfunction EH_applyProxy() {\n var p = EH_input('ehProxyInput').trim();\n if (!p) { EH_set('ehProxy', ''); EH_toast('✅ 已清空代理(直连\/VPN 全局模式)'); EH_refresh(); return; }\n if (!\/^(https?|socks5|socks4):\\\/\\\/\/i.test(p)) p = 'http:\/\/' + p;\n EH_set('ehProxy', p);\n EH_toast('✅ 代理已设为 ' + p + ';请点「🔍检测连通」验证');\n EH_refresh();\n}\nfunction EH_clearCookie() {\n EH_set('ehCookie', ''); EH_set('ehCookieInput', '');\n EH_toast('✅ 已清除面板 Cookie(游客模式)');\n EH_refresh();\n}\nfunction EH_switchHost(n) {\n var d = (Number(n) === 1) ? 'https:\/\/exhentai.org' : 'https:\/\/e-hentai.org';\n EH_set('ehHost', d);\n EH_toast('✅ 已切换到 ' + d + ';重新进入订阅\/发现页生效');\n EH_refresh();\n}\n\/\/ ---- 🧹 清域 Cookie + 🩺 一键自愈(核心修复) ----\nfunction EH_purge() {\n try { cookie.removeCookie('https:\/\/exhentai.org\/'); } catch (e) {}\n try { cookie.removeCookie('https:\/\/e-hentai.org\/'); } catch (e) {}\n EH_toast('✅ 已清 App 内 hentai 域 Cookie(含失效的 igneous=mystery \/ yay \/ sk)');\n}\nfunction EH_heal() {\n EH_toast('🩺 开始自愈:清域 Cookie → 只发两项重新触发站方签发 igneous…');\n EH_purge();\n var body = '';\n try { body = String(java.ajax(EH_ajaxUrl('\/'))); } catch (e) { EH_toast('❌ 自愈请求异常 ' + String(e).slice(0, 60)); return; }\n if (body.length > 5000 && body.indexOf('<') === 0) {\n var g = (body.match(\/\\\/g\\\/\\d+\\\/[0-9a-f]{10}\\\/\/g) || []).length;\n EH_toast('✅ 自愈成功:拿到 ' + body.length + 'B 完整首页,识别到 ' + g + ' 个画廊!igneous 已重新签发,请重新进入订阅页刷新');\n } else if (\/temporarily banned|excessive request\/i.test(body)) {\n EH_toast('❌ 出口 IP 被限频(24h)⇒ 换 VPN 节点后重试');\n } else {\n EH_toast('❌ 仍是空白页(' + body.length + 'B)⇒ 确认 Cookie 有效、VPN 出口干净,或稍等 5 分钟冷却再试');\n }\n EH_refresh();\n}\n\/\/ ---- 🔍 检测连通(用 java.ajax 纯净栈,避开 connect 的 CookieJar 污染) ----\nfunction EH_check() {\n var d = EH_host();\n EH_toast('🔍 正在检测 ' + d + '(' + (EH_gv('ehProxy', '') || '直连\/VPN') + ')…');\n var body = '';\n try { body = String(java.ajax(EH_ajaxUrl('\/'))); } catch (e) { body = String(e); }\n if (\/Exception|timeout|Failed to connect\/i.test(body.slice(0, 200))) {\n EH_toast('❌ 网络不通(' + body.slice(0, 70) + ')⇒ 检查 VPN \/ 代理'); return;\n }\n if (\/temporarily banned|excessive request|Your IP address has been\/i.test(body)) {\n EH_toast('❌ 出口 IP 被限频(24h 自动解封)⇒ 换 VPN 节点'); return;\n }\n if (body.length < 1000) {\n EH_toast('❌ 空白页(' + body.length + 'B)= igneous=mystery 白屏 ⇒ 点「🩺 一键自愈」清域重签'); return;\n }\n var galleries = (body.match(\/\\\/g\\\/\\d+\\\/[0-9a-f]{10}\\\/\/g) || []).length;\n var layout = \/class=\"itg glt([cemtl])\"\/.exec(body);\n EH_toast('✅ 连通成功(' + body.length + 'B);画廊链接 ' + galleries + ' 个,布局=glt' + (layout ? layout[1] : '?') + ';请重新进入订阅页看列表');\n}\n\/\/ ---- 🖼 正文每行图片数切换(1 图大图 \/ 2 图紧凑) ----\n\/\/ 用户 261107 反馈「一行两个有点小」⇒ 默认改为 1 图;按钮在两档间切换。\n\/\/ content.js 用 source.get('ehCols') 读档位分支 CSS;订阅源正文环境 source=RssSource 本体,\n\/\/ get\/put 直接可用(261030 源码级实锤,勿改用 getVariable)。\nfunction EH_cols() {\n var c = EH_gv('ehCols', '1');\n return (c === '2') ? '2' : '1';\n}\nfunction EH_toggleCols() {\n var old = EH_cols(), nw = (old === '1') ? '2' : '1';\n EH_set('ehCols', nw);\n EH_toast('🖼 已切换为「一行 ' + nw + ' 图」:' +\n (nw === '1' ? '单列大图(默认,图更大)' : '双列紧凑(一屏看更多)') +\n '。★已打开的文章有正文缓存,请退出重进本文或清除存档后生效');\n EH_refresh();\n}\nfunction EH_colsBtn() {\n try {\n\n var c = EH_cols();\n return (\"'🖼 正文每行 \" + c + \" 图(点击切为 \" + (c === '1' ? '2' : '1') + \" 图)'\") || \"'🖼 正文图片布局'\";\n\n } catch (eVN) { return \"'🖼 正文图片布局'\"; }\n}\nfunction EH_status() {\n \/\/ ★261114 v5.3b:整函数 try\/catch —— viewName 求值环境里任何异常都让串变空 → \"null\"\n try {\n var ck = EH_gv('ehCookie', ''), px = EH_gv('ehProxy', '');\n var st = (ck.indexOf('ipb_member_id=') >= 0 ? 'member✔ ' : 'member✘ ') +\n (ck.indexOf('ipb_pass_hash=') >= 0 ? 'pass✔' : 'pass✘');\n var acct = EH_creds().u ? '账号✔' : '账号✘';\n \/\/ ★261109 v3:把分辨率档位也写进「当前配置」,避免面板与正文实际档位不一致却看不出来\n var res = '分辨率?';\n try { res = EH_resCfg().n; } catch (eR) { res = EH_gv('ehResLabel', '分辨率?'); }\n return (\"'\" + EH_host().replace(\/^https?:\\\/\\\/\/, '') + ' | ' + (px ? '代理✔' : '直连') +\n ' | ' + st + ' | ' + acct + ' | 每行' + EH_cols() + '图 | ' + res + \"'\") || \"'📊 当前配置'\";\n } catch (eBig2) { return \"'📊 当前配置'\"; }\n}\nfunction EH_refresh() {\n try { java.reLoginView(); } catch (e) {\n try { java.reLoginView(false); } catch (e2) { EH_toast('(若按钮文字未刷新,请重新打开本面板)'); }\n }\n}\n\/\/ login() 点「登录」时一次性把输入框全部落库(源码把 loginData 绑成 result)\nfunction login(e) {\n if (e != undefined) { EH_toast(String(e)); return; }\n \/\/ 点「登录」只持久化输入框(源码 putLoginInfo 已自动做),不自动发起网络登录:\n \/\/ 账号密码登录需用户主动点「🔐 用账号密码登录」,避免误触触发 CF\/验证码。\n EH_applyCookie(); EH_applyProxy();\n EH_toast('✅ 已保存面板输入。★账号密码登录请点「🔐 用账号密码登录」;白屏点「🩺 一键自愈」;最后「🔍 检测连通」');\n}\n\n\/\/ ==================== 🔐 账号密码登录(261107 新增) ====================\n\/\/ 协议实锤(ExH login 用户脚本 \/ Let's panda \/ xeHentai core.py 三方一致):\n\/\/ POST https:\/\/forums.e-hentai.org\/index.php?act=Login&CODE=01\n\/\/ Content-Type: application\/x-www-form-urlencoded\n\/\/ body: referer=...&b=&bt=&UserName=<用户名>&PassWord=<密码>&CookieDate=1\n\/\/ 成功判据:响应含 \"You are now logged in as\";Set-Cookie 给 ipb_member_id \/ ipb_pass_hash\n\/\/ ★ 但本环境 forums 域被 Cloudflare Turnstile 挡死(GET\/POST 一律 403 cf-mitigated: challenge,\n\/\/ 换 UA 无效)⇒ 纯 HTTP 自动登录不可行。正解 = java.startBrowserAwait 打开内置浏览器,\n\/\/ 用户手动过盾 + 登录,回来后从 CookieStore 抽取两项写回 ehCookie。\n\/\/ ★ Cookie 按二级域归并:forums.e-hentai.org 与 e-hentai.org 同域共享,\n\/\/ 但 exhentai.org 是另一个二级域 ⇒ 必须显式 cookie.setCookie 复制过去。\nvar EH_FORUM = 'https:\/\/forums.e-hentai.org\/index.php?act=Login&CODE=00';\nvar EH_LOGIN_POST = 'https:\/\/forums.e-hentai.org\/index.php?act=Login&CODE=01';\n\nfunction EH_creds() {\n var u = EH_input('ehUser'), p = EH_input('ehPass');\n return { u: String(u || '').trim(), p: String(p || '').trim() };\n}\n\/\/ 从任意 Cookie 串里抽两项(含 URL 编码\/引号容错)\nfunction EH_extract2(raw) {\n var s = String(raw || '').replace(\/\\r\/g, '').replace(\/\\n\/g, ' ');\n var parts = s.split(';'), keep = {}, i, q, eq, k, v;\n for (i = 0; i < parts.length; i++) {\n q = parts[i].trim(); eq = q.indexOf('=');\n if (eq < 1) continue;\n k = q.substring(0, eq).trim().replace(\/^[\"']|[\"']$\/g, '');\n v = q.substring(eq + 1).trim().replace(\/^[\"']|[\"']$\/g, '');\n if ((k === 'ipb_member_id' || k === 'ipb_pass_hash') && v && !keep[k]) keep[k] = v;\n }\n return keep;\n}\n\/\/ 把抽到的两项写进:① 源变量 ehCookie ② exhentai.org 域持久 Cookie\nfunction EH_commitCreds(keep, from) {\n if (!keep.ipb_member_id || !keep.ipb_pass_hash) return false;\n var two = 'ipb_member_id=' + keep.ipb_member_id + '; ipb_pass_hash=' + keep.ipb_pass_hash + '; inline_set=dm_l';\n EH_set('ehCookie', two);\n EH_set('ehCookieInput', two);\n try { cookie.setCookie('https:\/\/exhentai.org\/', two); } catch (e) {}\n \/\/ 清掉可能残留的 mystery,让站方重新签发\n try { cookie.removeCookie('https:\/\/exhentai.org\/'); } catch (e) {}\n try { cookie.setCookie('https:\/\/exhentai.org\/', two); } catch (e) {}\n EH_toast('✅ ' + from + ' 登录成功!已取得 ipb_member_id=' + keep.ipb_member_id +\n ',Cookie 已写入源变量与里站域。请点「🔍 检测连通」验证,再重进订阅页');\n EH_refresh();\n return true;\n}\n\/\/ 通道 A:直接 POST(出口没被 CF 挡时最快)\nfunction EH_tryPost(c) {\n var body = 'referer=https:\/\/forums.e-hentai.org\/index.php?&b=&bt=&UserName=' +\n encodeURIComponent(c.u) + '&PassWord=' + encodeURIComponent(c.p) + '&CookieDate=1';\n var opt = '{\"method\":\"POST\",\"body\":' + JSON.stringify(body) +\n ',\"headers\":{\"User-Agent\":' + JSON.stringify(EH_UA) +\n ',\"Content-Type\":\"application\/x-www-form-urlencoded\"' +\n ',\"Referer\":\"https:\/\/forums.e-hentai.org\/index.php?act=Login&CODE=00\"}}';\n var r, b = '';\n try {\n r = java.connect(EH_LOGIN_POST + ',' + opt, '', 20000);\n b = String(r.body());\n } catch (e) { return { ok: false, reason: 'POST异常 ' + String(e).slice(0, 50) }; }\n if (\/Just a moment|cf-mitigated|challenge|Checking your browser\/i.test(b)) {\n return { ok: false, reason: 'Cloudflare 挑战页(需内置浏览器过盾)' };\n }\n if (\/You are now logged in as|Logged in as|logout|Log Out\/i.test(b)) {\n \/\/ 从响应头 Set-Cookie 或 CookieStore 取\n var raw = '';\n try { raw = String(r.headers()); } catch (e) {}\n if (!raw) { try { raw = String(cookie.getCookie('https:\/\/forums.e-hentai.org\/')); } catch (e2) {} }\n var k = EH_extract2(raw);\n if (!k.ipb_member_id) { try { k = EH_extract2(cookie.getCookie('https:\/\/forums.e-hentai.org\/')); } catch (e3) {} }\n if (k.ipb_member_id && k.ipb_pass_hash) return { ok: true, keep: k };\n return { ok: false, reason: '登录成功但未读到 Cookie(检查 CookieJar)' };\n }\n return { ok: false, reason: '账号密码错误 \/ 需验证码 \/ 未识别响应(' + b.length + 'B)' };\n}\n\/\/ 通道 B:内置浏览器(过 CF + 手动登录,用户填完点页面里的登录即可)\nfunction EH_browserLogin() {\n EH_toast('🌐 正在打开论坛登录页…请在内置浏览器里完成 Cloudflare 验证并登录账号,登录后返回本面板再点「🔄 读取登录 Cookie」');\n try {\n var r = java.startBrowserAwait(EH_FORUM, 'E-Hentai 论坛登录(过CF验证并登录)', false);\n EH_toast('🌐 浏览器已返回(' + String(r.body()).length + 'B)。请点「🔄 读取登录 Cookie」把登录态写进本源');\n } catch (e) {\n EH_toast('❌ 打开浏览器失败:' + String(e).slice(0, 80));\n }\n}\n\/\/ 通道 C:从浏览器登录后的 CookieStore 读取并写回\nfunction EH_readBrowserCookie() {\n var raws = [\n 'https:\/\/forums.e-hentai.org\/', 'https:\/\/e-hentai.org\/', 'https:\/\/exhentai.org\/'\n ], i, k = {};\n for (i = 0; i < raws.length; i++) {\n try {\n var s = String(cookie.getCookie(raws[i]));\n var q = EH_extract2(s);\n if (q.ipb_member_id && !k.ipb_member_id) k.ipb_member_id = q.ipb_member_id;\n if (q.ipb_pass_hash && !k.ipb_pass_hash) k.ipb_pass_hash = q.ipb_pass_hash;\n } catch (e) {}\n }\n if (EH_commitCreds(k, '浏览器登录')) return;\n EH_toast('⚠ 未读到 ipb_member_id \/ ipb_pass_hash。请确认已在内置浏览器里登录成功(页面显示用户名),或改用下方「粘贴 Cookie」通道');\n}\n\/\/ 主入口:先直接 POST,遇 CF 自动转内置浏览器\nfunction EH_pwdLogin() {\n var c = EH_creds();\n if (!c.u || !c.p) { EH_toast('⚠ 请先在上面填「账号」和「密码」'); return; }\n EH_toast('🔐 正在用账号密码登录论坛…');\n var r = EH_tryPost(c);\n if (r.ok) { EH_commitCreds(r.keep, '账号密码'); return; }\n EH_toast('⚠ 直接登录未成功:' + r.reason + ' ⇒ 转为内置浏览器登录');\n EH_browserLogin();\n}\nfunction EH_loginStatus() {\n try {\n\n var c = EH_creds();\n return (\"'🔐 账号 \" + (c.u ? '已填(' + c.u.slice(0, 8) + '…)' : '未填') +\n ' \/ 密码 ' + (c.p ? '已填' : '未填') + \" · 点我用账号密码登录'\") || \"'🔐 用账号密码登录'\";\n\n } catch (eVN) { return \"'🔐 用账号密码登录'\"; }\n}\n\n\/\/ ==================== 📝 账号注册(261107 新增) ====================\n\/\/ 官方入口(EHWiki \/ 注册教程 \/ e站食用指北 三方一致):\n\/\/ https:\/\/forums.e-hentai.org\/index.php?act=Reg&CODE=00 (论坛 → Register → 同意条款 → 填表)\n\/\/ 字段:Login Username(登录用,唯一) \/ Display Name(显示名) \/ Password \/ Email \/ Security Code(CAPTCHA)\n\/\/ ★ EHWiki:注册免费、不发验证邮件、账号立即可用;但每人只允许 1 个活跃账号,多开会被封。\n\/\/ ★ 论坛域被 Cloudflare Turnstile 挡(本次实测 GET\/POST 一律 403 challenge)+\n\/\/ 注册表单含 CAPTCHA ⇒ **纯 HTTP 自动注册不可行**,必须内置浏览器人工完成。\n\/\/ 源码实锤:WebViewActivity.onPageFinished 每次把 android.webkit.CookieManager 的 Cookie\n\/\/ 回写 CookieStore.setCookie ⇒ startBrowserAwait 回来后 EH_readBrowserCookie 能读到 ipb_*。\nvar EH_REG_URL = 'https:\/\/forums.e-hentai.org\/index.php?act=Reg&CODE=00';\nvar EH_FORGOT_URL = 'https:\/\/forums.e-hentai.org\/index.php?act=Reg&CODE=10';\n\n\/\/ 打开注册页(内置浏览器;用户手动过盾 + 同意条款 + 填表 + 输验证码 + 提交)\nfunction EH_register() {\n EH_toast('📝 正在打开论坛注册页…请先把浏览器首选语言设为 English;依次完成 Cloudflare 验证 → 勾选同意条款 → 填写 Login Username\/Display Name\/Password\/Email\/Security Code → 提交');\n try {\n \/\/ refetchAfterSuccess=false:只取回页面,不额外重发(避免刚注册完又撞 CF)\n var r = java.startBrowserAwait(EH_REG_URL, '📝 E-Hentai 账号注册(过CF+填表+验证码)', false);\n EH_toast('📝 注册页已返回(' + String(r.body()).length + 'B)。★注册成功后请在同一浏览器里登录一次,回到本面板点「🔄 读取登录 Cookie」');\n } catch (e) {\n EH_toast('❌ 打开注册页失败:' + String(e).slice(0, 90));\n }\n}\n\/\/ 忘记密码(官方找回表单,同一浏览器通道)\nfunction EH_forgot() {\n EH_toast('🔑 正在打开找回密码表单(需用户名或注册邮箱)…');\n try {\n var r = java.startBrowserAwait(EH_FORGOT_URL, '🔑 E-Hentai 找回用户名\/密码', false);\n EH_toast('🔑 找回页已返回(' + String(r.body()).length + 'B)');\n } catch (e) {\n EH_toast('❌ 打开失败:' + String(e).slice(0, 90));\n }\n}\n\/\/ 注册\/登录后的收尾:读 Cookie → 写回源 → 验证连通(一条龙)\nfunction EH_afterBrowser() {\n EH_readBrowserCookie();\n \/\/ 顺便测一次,给用户明确反馈\n var body = '';\n try { body = String(java.ajax(EH_ajaxUrl('\/'))); } catch (e) { body = ''; }\n if (body.length > 5000 && body.indexOf('<') === 0) {\n EH_toast('✅ 验证通过:拿到 ' + body.length + 'B 首页 HTML,账号可用!请重新进入订阅页');\n } else {\n EH_toast('⚠ 首页仍是空白(' + body.length + 'B)。新注册账号可能需等待审核\/换干净 IP,或点「🩺 一键自愈」');\n }\n}\n\n\/\/ ==================== 261109 v3 增量:分辨率档位 \/ 线路测速 \/ 登录态检查 ====================\n\/\/ 兼容要点(源码级实锤,261109 用户纠正后复核):\n\/\/ ★★ E 版默认分支是 main(不是 master!)。main 的 RowUi.kt 明确支持\n\/\/ select\/toggle\/viewName\/chars\/default,SourceLoginDialog.kt 与官方版同尺寸(34267B);\n\/\/ 实例「🍥哔咔漫画」loginUi 里就有 {\"type\":\"select\",\"chars\":[...],\"default\":...,\"action\":...}。\n\/\/ ⇒ 分辨率选择只用 select 下拉(双版本都可用)。\n\/\/ ★ select 选中值写进 loginInfo[name],handleButtonClick 的 result=getLoginData()\n\/\/ = viewModel.loginInfo.toMutableMap() ⇒ action 里读 result['ehResSelect'] 即当前档位。\n\/\/ (哔咔实例读 infoMap[...] 亦可,二者都绑;本源统一用 result 通道,与 EH_input 一致。)\n\/\/ ★ 面板 action 环境 java = SourceLoginJsExtensions(ajax\/connect\/toast\/startBrowserAwait 可用),\n\/\/ 且 source.put\/get 可落库(261107 已实锤,沿用现有 EH_set\/EH_gv)。\n\/\/ ★ POST \/uconfig.php 是写账号设置:必须用户主动点「同步站点档位」才发,绝不静默改。\n\/\/ ★ 裸 Cookie 覆盖 uconfig\/xres 无效(261109 三组对照实锤)⇒ 服务端分辨率只能靠 POST。\n\/\/ ★ 站点真实表单:xr=0自动 \/ 1=780x \/ 3=1280x(4\/5 需 Hath 已 disabled);ts=0\/1\/2。\n\n\/\/ ---- 分辨率档位表 ----\n\/\/ hd = ruleContent 阶段服务端内联高清张数(Rhino java.ajax,两版本都可用;60 封顶防站方限频)\n\/\/ win = WebView 视口懒加载并发窗口\n\/\/ gap = 每次 \/s\/ 请求间隔 ms(越小越快但越易触发站方短时限频)\n\/\/ margin = IntersectionObserver 预取视口外边距\n\/\/ orig = 是否在每格附「原图」链接\n\/\/ xr\/ts = 同步到站点 uconfig.php 的真实表单值\n\/\/ ★ HD_MAX 克制在 0\/6\/15\/25:站方对短时密集 \/s\/ 请求敏感(限频约 24h),\n\/\/ 不因「超清档」把并发拉到 60 张;首屏之外的图一律交给 WebView 视口懒加载。\nvar EH_RES = [\n { k: '0', n: '省流·缩略图≈250px', hd: 0, gap: 1000, margin: '0px', xr: '1', ts: '0' },\n { k: '1', n: '自动·均衡≈780x', hd: 6, gap: 700, margin: '600px', xr: '0', ts: '0' },\n { k: '2', n: '高清·1280x(WiFi)', hd: 15, gap: 450, margin: '1200px', xr: '3', ts: '2' },\n { k: '3', n: '超清·全量·大屏', hd: 25, gap: 350, margin: '1800px', xr: '3', ts: '2' }\n];\nfunction EH_resCfg() {\n \/\/ 主存档 ehRes(由 select 的 EH_setRes 写入);\n \/\/ 兼容:若面板 select 已选但 action 未触发,可从 result\/infoMap 的标签反查档位。\n \/\/ ★261114 v5.3b:整个函数体包 try\/catch —— 本函数在 viewName 求值环境里被\n \/\/ EH_resStatus\/EH_status 调用,任何一步异常都会让整个求值串变空 → 按钮显示 \"null\"。\n try {\n var k = EH_gv('ehRes', '1');\n var label = '';\n try { if (typeof result !== 'undefined' && result != null) {\n if (typeof result.get === 'function') { try { label = String(result.get('ehResSelect') || ''); } catch (e00) {} }\n if (!label) { try { label = String(result['ehResSelect'] || ''); } catch (e01) {} }\n } } catch (e0) {}\n if (!label) { try { if (typeof infoMap !== 'undefined' && infoMap) label = String(infoMap['ehResSelect'] || ''); } catch (e1) {} }\n if (label) {\n for (var i = 0; i < EH_RES.length; i++) {\n if (label.indexOf(EH_RES[i].n) >= 0) { k = EH_RES[i].k; break; }\n }\n }\n for (var j = 0; j < EH_RES.length; j++) if (EH_RES[j].k === k) return EH_RES[j];\n return EH_RES[1];\n } catch (eBig) { return EH_RES[1]; }\n}\n\/\/ select 的默认档位(供状态\/说明用;default 字段本身是纯字符串,不走 JS 求值)\nfunction EH_resDefault() {\n return EH_resCfg().n;\n}\n\/\/ ① 分辨率选择:select 下拉的 action(用户切换档位时由 SourceLoginDialog 自动回调)\n\/\/ ★ 选中值经 loginInfo → result 传来;同时兼容 infoMap 与 source.get 三通道。\nfunction EH_setRes() {\n EH_toast('🖼 正在应用分辨率档位…');\n var label = '';\n try { label = String(result['ehResSelect'] || ''); } catch (e0) { label = ''; }\n if (!label) { try { label = String(infoMap['ehResSelect'] || ''); } catch (e1) { label = ''; } }\n if (!label) { try { label = String(result.get ? result.get('ehResSelect') : '') || ''; } catch (e2) { label = ''; } }\n var hit = null;\n for (var i = 0; i < EH_RES.length; i++) if (label.indexOf(EH_RES[i].n) >= 0) hit = EH_RES[i];\n if (!hit) {\n EH_toast('⚠ 未识别档位「' + label + '」,保持当前设置;请重新打开面板再选一次');\n return;\n }\n EH_set('ehRes', hit.k);\n EH_set('ehResLabel', hit.n);\n EH_toast('✅ 已选【' + hit.n + '】:首屏内联 ' + hit.hd + ' 张高清 \/ 视口懒加载串行间隔 ' +\n hit.gap + 'ms \/ 预取 ' + hit.margin + ' \/ 仅加载当前可见页');\n EH_toast((hit.hd === 0 ? 'ℹ 省流档只用 250px 缩略图、不请求 \/s\/ 页,最不易触发站方限频;' :\n 'ℹ 高清档会在图片进入视口时按间隔逐张请求 \/s\/ 页,网络差建议降档;') +\n '★已打开的文章有正文缓存,请退出重进本文或清存档后生效');\n EH_toast('🌐 若要站方 \/s\/ 页直接返回该分辨率,再点「⚙ 同步站点分辨率(xr)」(会写入账号 uconfig,需你主动点)');\n}\nfunction EH_resStatus() {\n try {\n\n var c = EH_resCfg(), s = EH_gv('ehXR', '未同步');\n var L = EH_gv('ehResLabel', '');\n return (\"'📊 当前:分辨率【\" + c.n + \"】\" + (L && L !== c.n ? '(存:' + L + ')' : '') +\n \" · 每行\" + EH_cols() + \"图 · 站点xr=\" + s + \"'\") || \"'📊 分辨率状态'\";\n\n } catch (eVN) { return \"'📊 分辨率状态'\"; }\n}\n\/\/ ② 同步站点分辨率(POST \/uconfig.php;全阶段 Toast + 回读确认)\nfunction EH_uconfigSync() {\n var c = EH_resCfg(), d = EH_host();\n EH_toast('⚙ 开始同步站点分辨率:准备向 ' + d + '\/uconfig.php 提交 xr=' + c.xr + ' ts=' + c.ts +\n '(会修改你的 E-Hentai 账号设置,仅本次主动点击执行)…');\n var body = 'xr=' + c.xr + '&ts=' + c.ts + '&apply=Apply';\n var opt = '{\"method\":\"POST\",\"body\":' + JSON.stringify(body) +\n ',\"headers\":{\"User-Agent\":' + JSON.stringify(EH_UA) +\n ',\"Cookie\":' + JSON.stringify(EH_cookie(EH_gv('ehCookie', ''))) +\n ',\"Content-Type\":\"application\/x-www-form-urlencoded\"' +\n ',\"Referer\":' + JSON.stringify(d + '\/uconfig.php') + '}}';\n EH_toast('⚙ 正在提交设置(最多 20s)…');\n var r = '', b = '';\n try { r = java.connect(d + '\/uconfig.php,' + opt, '', 20000); b = String(r.body()); }\n catch (e) { EH_toast('❌ 提交失败:' + String(e).slice(0, 90) + ' ⇒ 检查代理\/Cookie,或改用面板档位(客户端侧已生效,不影响使用)'); return; }\n if (\/Just a moment|cf-mitigated|challenge\/i.test(b)) {\n EH_toast('❌ 被 Cloudflare 挑战挡下 ⇒ 请用「🌐 打开论坛登录」过盾后重试;面板档位不受影响'); return;\n }\n if (!\/Settings were updated|Settings Updated\/i.test(b)) {\n EH_toast('⚠ 未读到「Settings were updated」(' + b.length + 'B)⇒ 可能 Cookie 失效或账号无权限;面板档位仍已生效'); return;\n }\n EH_toast('✅ 站点设置已更新:xr=' + c.xr + '(' + (c.xr === '0' ? '自动1280x\/移动780x' : c.xr === '1' ? '780x 省流' : '1280x 高清') +\n ')、ts=' + c.ts + ';正在回读确认…');\n \/\/ 回读确认(只读,一次)\n var chk = '';\n try {\n var h = { 'User-Agent': EH_UA, 'Cookie': EH_cookie(EH_gv('ehCookie', '')), 'Referer': d + '\/uconfig.php' };\n chk = String(java.ajax(d + '\/uconfig.php,{\"headers\":' + JSON.stringify(h) + '}'));\n } catch (e2) { chk = ''; }\n var m = chk.match(\/<input[^>]*name=[\"']xr[\"'][^>]*checked[^>]*>\/i);\n var got = m ? ((m[0].match(\/value=[\"'](\\d+)[\"']\/i) || [])[1] || '?') : '?';\n EH_set('ehXR', got);\n EH_toast(got === c.xr ? '✅ 回读确认成功:站点当前 xr=' + got + ',已与面板档位一致。★退出重进文章即可看到新分辨率'\n : '⚠ 回读 xr=' + got + '(期望 ' + c.xr + ')⇒ 账号可能禁用该档(4\/5需Hath),已记录;面板档位仍可用');\n EH_refresh();\n}\n\n\/\/ ---- 线路测速 \/ 切换(发布页式管理:测得的 ms 写进按钮文字;各阶段 Toast)----\n\/\/ 站点无官方「网址发布页」,只有表站\/里站两条固定线路 + 用户自填代理;\n\/\/ 这里把原「站点切换」升级为「测速 → 点击切换」的线路面板,满足线路选择诉求。\nfunction EH_pingOne(host) {\n var u = host + '\/,{\"headers\":' + JSON.stringify({ 'User-Agent': EH_UA, 'Cookie': EH_cookie(EH_gv('ehCookie', '')) }) + '}';\n var t0 = new Date().getTime(), b = '';\n try { b = String(java.connect(u, '', 8000)); }\n catch (e) { return { ms: -1, len: 0, err: String(e).slice(0, 50) }; }\n var ms = new Date().getTime() - t0;\n if (\/Exception|timeout|Failed to connect|SSLHandshake\/i.test(b.slice(0, 200))) return { ms: ms, len: 0, err: '网络异常' };\n return { ms: ms, len: b.length, err: '' };\n}\nfunction EH_testDelay() {\n EH_toast('⚡ 开始测试两条线路延时(各 1 次请求,间隔 800ms,不会频繁访问)…');\n var hs = ['https:\/\/exhentai.org', 'https:\/\/e-hentai.org'], rec = [], i;\n for (i = 0; i < hs.length; i++) {\n EH_toast('⚡ 正在测 (' + (i + 1) + '\/2) ' + hs[i] + ' …');\n var r = EH_pingOne(hs[i]);\n rec.push({ h: hs[i], ms: r.ms, len: r.len, err: r.err });\n EH_toast('⚡ ' + hs[i] + ' → ' + (r.ms >= 0 ? r.ms + 'ms' : '超时') +\n ' \/ ' + r.len + 'B' + (r.err ? '(' + r.err + ')' : ''));\n if (i === 0) { try { Packages.java.lang.Thread.sleep(800); } catch (eS) {} }\n }\n var ok = [];\n for (i = 0; i < rec.length; i++) if (rec[i].ms >= 0 && rec[i].len > 1000) ok.push(rec[i]);\n EH_set('ehPingEx', String((rec[0].ms >= 0 ? rec[0].ms : -1)));\n EH_set('ehPingTable', String((rec[1].ms >= 0 ? rec[1].ms : -1)));\n EH_set('ehPingText', '里站 ' + (rec[0].ms >= 0 ? rec[0].ms + 'ms' : '超时') +\n ' \/ 表站 ' + (rec[1].ms >= 0 ? rec[1].ms + 'ms' : '超时'));\n EH_toast(ok.length ? '✅ 测速完成:' + EH_gv('ehPingText', '') + '。★延时已写入下方两个线路按钮,点按钮即切换(建议选 B 数大且延时小的)'\n : '❌ 两条线路都没拿到完整页 ⇒ 检查 VPN \/ 代理,或点「🩺 一键自愈」;测速结果仍已记录');\n EH_refresh();\n}\nfunction EH_hostBtn(n) {\n try {\n\n var d = (Number(n) === 1) ? '里站 exhentai.org' : '表站 e-hentai.org';\n var ms = EH_gv(Number(n) === 1 ? 'ehPingEx' : 'ehPingTable', '');\n var cur = (EH_host().indexOf(Number(n) === 1 ? 'exhentai' : 'e-hentai') >= 0) ? '【当前】' : '';\n return (\"'\" + cur + d + (ms !== '' && ms !== '-1' ? ' · ' + ms + 'ms' : ' · 未测速') + \"(点击切换)'\") || \"'切换线路'\";\n\n } catch (eVN) { return \"'切换线路'\"; }\n}\nfunction EH_switchHostEx(n) {\n EH_toast('🔀 正在切换线路到 ' + (Number(n) === 1 ? '里站 exhentai.org' : '表站 e-hentai.org') + '…');\n EH_switchHost(n); \/\/ 复用原函数(内含 Toast + refresh)\n EH_toast('ℹ 切换完成。★重新进入订阅\/发现页生效;建议点「⚡ 测试各线路延时」复核新线路');\n}\n\n\/\/ ---- ③ 明确的「检查登录状态」按钮(须登录站点要求)----\nfunction EH_checkLogin() {\n var d = EH_host();\n EH_toast('✅ 正在检查登录状态:请求 ' + d + ' 首页(单次,不触发搜索)…');\n var body = '';\n try { body = String(java.ajax(EH_ajaxUrl('\/'))); }\n catch (e) { EH_toast('❌ 请求异常:' + String(e).slice(0, 80) + ' ⇒ 检查 VPN\/代理'); return; }\n if (\/Exception|timeout|Failed to connect|SSLHandshake\/i.test(body.slice(0, 200))) {\n EH_toast('❌ 网络不通(DNS 污染 \/ SNI 封锁 \/ 代理未生效)⇒ 填代理或换 VPN 节点'); return;\n }\n if (\/temporarily banned|excessive request|Your IP address has been\/i.test(body)) {\n EH_toast('❌ 出口 IP 被站方限频(约 24h 解封)⇒ 换 VPN 节点,勿频繁刷新'); return;\n }\n if (body.length < 1000) {\n EH_toast('❌ 空白页(' + body.length + 'B)⇒ 先在上方粘贴【你自己账号】的完整 Cookie 并点「💾 保存 Cookie」;再点「🔄 刷新 igneous」。★本源不再内置任何测试账号'); return;\n }\n if (\/Sad Panda|sadpanda|You do not have access\/i.test(body)) {\n EH_toast('❌ Sad Panda(账号无里站权限)⇒ 需已审核的 E-Hentai 账号 Cookie'); return;\n }\n var g = (body.match(\/\\\/g\\\/\\d+\\\/[0-9a-f]{10}\\\/\/g) || []).length;\n var logged = \/ipb_member_id|logged in|Log Out|logout|My Home|Hath Perks\/i.test(body);\n EH_toast('✅ 已登录且可用:' + body.length + 'B 首页 \/ ' + g + ' 个画廊' +\n (logged ? ' \/ 页面含登录态标识' : '') + '。★可正常浏览;分辨率档位=' + EH_resCfg().n);\n}\n\/\/ ---- ⑧ 发布页输入框 → 一键取线路(E-Hentai 无官方「网址发布页」,明确降级不臆造) ----\nfunction EH_fromPub() {\n EH_toast('🔗 正在读取「发布页网址输入框」…');\n var u = EH_input('ehPubInput').trim();\n if (!u) {\n EH_toast('ℹ 输入框为空:E-Hentai \/ ExHentai 官方没有「网址发布页」,只有表站\/里站两条固定线路;已为你改用下方两条,并开始测速…');\n EH_testDelay();\n return;\n }\n if (!\/^https?:\\\/\\\/\/i.test(u)) u = 'https:\/\/' + u;\n EH_toast('🔗 读到自定义线路:' + u + ',正在测速(单次,不频繁访问)…');\n var r = EH_pingOne(u.replace(\/\\\/+$\/, ''));\n if (r.ms >= 0 && r.len > 1000) {\n EH_set('ehHost', u.replace(\/\\\/+$\/, ''));\n EH_set('ehPingText', '自定义 ' + r.ms + 'ms');\n EH_toast('✅ 自定义线路可用:' + r.ms + 'ms \/ ' + r.len + 'B,已切换为当前站点;请重新进入订阅页');\n } else {\n EH_toast('❌ 自定义线路不可用(' + (r.ms >= 0 ? r.ms + 'ms \/ ' + r.len + 'B' : '超时') +\n ')⇒ 未切换,仍用当前线路;可点「⚡ 测试各线路延时」核对表\/里站');\n }\n EH_refresh();\n}\n\n\n\/\/ ==================== 📁 正文显示收藏夹(261113 v4 新增) ====================\n\/\/ 开关存 ehFavPanel('1'=正文信息下方显示收藏夹列表 \/ '0'=不显示)。\n\/\/ 正文 ruleContent 读该变量:开启时才请求 1 次 favorites.php 抓夹名单,\n\/\/ 渲染 10 个夹子 chip;点击 chip 页内直接 POST gallerypopups.php addfav\n\/\/ (加入\/移出即时生效,不重载正文、不失效缓存)。\nfunction EH_favToggle() {\n var cur = EH_gv('ehFavPanel', '0');\n var nw = (cur === '1') ? '0' : '1';\n EH_set('ehFavPanel', nw);\n EH_toast(nw === '1'\n ? '📁 已开启:正文信息下方将显示你的收藏夹列表(点击夹子加入\/再点移出当前画廊)。★已打开的文章有正文缓存,退出重进生效'\n : '📁 已关闭:正文不再显示收藏夹列表。★已打开的文章退出重进生效');\n EH_refresh();\n}\nfunction EH_favBtn() {\n try {\n\n var on = EH_gv('ehFavPanel', '0') === '1';\n return (\"'📁 正文显示收藏夹:\" + (on ? '开(点击关闭)' : '关(点击开启)') + \"'\") || \"'📁 正文显示收藏夹'\";\n\n } catch (eVN) { return \"'📁 正文显示收藏夹'\"; }\n}\n\n\n\/\/ ==================== 🏷 tag 点击模式(261114 v5 新增) ====================\n\/\/ 两个独立开关:ehTagSearch(点 tag 名字搜索对应 tag)\/ ehTagWatch(点 +\/✓ 订阅对应 tag)。\n\/\/ 可组合:都开时名字可点搜索、旁边独立 chip 订阅,互不抢点击。\n\/\/ 订阅协议实锤:POST \/mytags add(tagwatch_new=on) \/ mass(modify_usertags[]=id) 取消;\n\/\/ 两者都返回 302,成功判据=回读 \/mytags 出现\/消失(沙盒真机闭环)。\nfunction EH_tagToggle(k) {\n var nw = (EH_gv(k, '0') === '1') ? '0' : '1';\n EH_set(k, nw);\n var n = (k === 'ehTagSearch') ? '点 tag 搜索' : '订阅 tag';\n EH_toast('🏷 ' + n + ':' + (nw === '1' ? '已开启' : '已关闭') +\n '。★已打开的文章有正文缓存,退出重进生效');\n EH_refresh();\n}\nfunction EH_tagBtn(k, name) {\n try {\n\n var on = EH_gv(k, '0') === '1';\n return (\"'🏷 \" + name + \":\" + (on ? '开(点击关闭)' : '关(点击开启)') + \"'\") || \"'🏷 tag 开关'\";\n\n } catch (eVN) { return \"'🏷 tag 开关'\"; }\n}\n\n\n\/\/ ==================== 🔄 刷新 igneous(261114 v5.1 登录态持久化) ====================\n\/\/ 站方对「只发两项」的请求会 Set-Cookie 一个 30 天有效 igneous(同账号重签同值)。\n\/\/ enabledCookieJar=false 后不再自动回存,故这里用 java.connect(返回 StrResponse,\n\/\/ 有 headers())抓 Set-Cookie 存进源变量 ehIgneous;header\/loginCheckJs\/正文四处统一读它。\n\/\/ ★ 绝不存 'mystery'(站方 Sad Panda 页下发的失效值,带上就 0B 白屏)。\nfunction EH_refreshIgneous() {\n EH_toast('🔄 正在请求里站首页,读取站方签发的有效 igneous…');\n var u = EH_host() + '\/,{\"headers\":' + JSON.stringify({\n 'User-Agent': EH_UA,\n 'Cookie': EH_cookie(EH_gv('ehCookie', '')),\n 'Referer': 'https:\/\/exhentai.org\/'\n }) + '}';\n var hdr = '', body = '';\n try {\n var r = java.connect(u, '', 20000);\n try { hdr = String(r.headers()); } catch (e1) { hdr = ''; }\n try { body = String(r.body()); } catch (e2) { body = ''; }\n } catch (e) { EH_toast('❌ 请求异常:' + String(e).slice(0, 90) + ' ⇒ 检查代理\/VPN'); return; }\n var m = \/igneous=([A-Za-z0-9]{6,40})\/.exec(hdr);\n if (m && m[1] !== 'mystery') {\n EH_set('ehIgneous', m[1]);\n EH_toast('✅ 已保存有效 igneous=' + m[1] + '(30天有效)。以后打开订阅源无需再清 Cookie 重登;退出重进文章生效');\n } else if (body.length > 5000) {\n \/\/ 站方这次没重签(说明请求里已带有效 igneous)→ 沿用当前值即可\n var cur = EH_gv('ehIgneous', '');\n EH_toast('✅ 首页正常(' + body.length + 'B);站方这次未重签 igneous' +\n (cur ? ',当前值 ' + cur.slice(0, 8) + '… 仍有效' : ',用空值占位即可,站方按需重签') + ',无需刷新');\n } else {\n EH_toast('⚠ 未读到有效 igneous(首页 ' + body.length + 'B)⇒ 点「🩺 一键自愈」后重试,或粘贴浏览器完整 Cookie');\n }\n EH_refresh();\n}\nfunction EH_igBtn() {\n try {\n\n var ig = EH_gv('ehIgneous', '');\n return (\"'🔄 刷新 igneous(当前 \" + (ig ? ig.slice(0, 8) + '…' : '空值占位·站方自动重签') + \")'\") || \"'🔄 刷新 igneous'\";\n\n } catch (eVN) { return \"'🔄 刷新 igneous'\"; }\n}\n\n\n\/\/ ==================== 🩺 登录态一键诊断(261114 v5.2) ====================\n\/\/ 用户报「收藏夹获取失败 \/ 订阅tag失败:检查登录状态 \/ 关注列表为空」时的定位按钮:\n\/\/ 分别打 \/watched \/favorites.php \/mytags,回报每页大小与站方提示,区分\n\/\/ 「没配Cookie」\/「igneous 无效」\/「该账号确实没有 watched tag」三种情况。\nfunction EH_diagLogin() {\n var ck = EH_gv('ehCookie', '');\n if (ck.indexOf('ipb_member_id=') < 0 || ck.indexOf('ipb_pass_hash=') < 0) {\n EH_toast('❌ 未检测到你自己的 Cookie ⇒ 请在面板「① 里站 Cookie」粘贴浏览器登录后复制的完整串并点「💾 保存 Cookie」。★本源不内置任何账号');\n return;\n }\n var ig = EH_gv('ehIgneous', '');\n EH_toast('🩺 正在诊断:Cookie 已配置(member=' + (\/ipb_member_id=(\\d+)\/.exec(ck) || [])[1] +\n '),igneous=' + (ig ? ig.slice(0, 8) + '…' : '空值占位') + ';依次请求 watched\/favorites\/mytags…');\n var paths = ['\/watched', '\/favorites.php', '\/mytags'], res = [], i;\n for (i = 0; i < paths.length; i++) {\n var b = '';\n try { b = String(java.ajax(EH_ajaxUrl(paths[i]))); } catch (e) { b = String(e); }\n var tag = \/Exception|timeout|Failed to connect\/i.test(b.slice(0, 200)) ? '网络异常' :\n \/temporarily banned|excessive request\/i.test(b) ? 'IP限频' :\n b.length < 1000 ? '空白页(登录态无效)' :\n \/You do not have any watched tags\/i.test(b) ? '该账号确实没有 watched tag' :\n \/No hits found\/i.test(b) ? '页内无结果' : '正常(' + b.length + 'B)';\n res.push(paths[i] + '→' + tag);\n if (i < paths.length - 1) { try { Packages.java.lang.Thread.sleep(400); } catch (eS) {} }\n }\n EH_toast('🩺 诊断结果:' + res.join(' \/ ') +\n '。★若 watched 报「没有 watched tag」= 订阅没成功,请先在正文点 tag 旁的 + 订阅,或到站内 My Tags 添加');\n}\nfunction EH_diagBtn() {\n return \"'🩺 登录态一键诊断(区分没配Cookie\/igneous无效\/无watched标签)'\" || \"'🩺 登录态一键诊断'\";\n}\n",
"preload": false,
"preloadJs": "window.java=java;window.source=source;window.cache=cache;",
"ruleArticles": "@css:table.itg > tbody > tr:not(:has(th)), table.itg > tr:not(:has(th))",
"ruleContent": "@js:\n(function () {\n \/\/ ============================================================\n \/\/ ExHentai 订阅源 · 正文规则(画廊详情页 → 自拼纯净 HTML)\n \/\/ 数据源 = result(App 已按 header.js 的 proxy\/Cookie 抓好的内容页 HTML 字符串)\n \/\/ 结构依据:Venera ehentai.js \/ H-Viewer 规则 \/ TachiyomiSY \/ RSSHub 源码 +\n \/\/ Keenable 实时页面文本(2026-11-07 交叉实锤)\n \/\/ ★ 不写死页码上限:后续缩略图页数完全由站点 #gdd 的 \"N pages\" 与\n \/\/ table.ptb 的最大 ?p=N 推导,站点以后加页会自动跟着变。\n \/\/ ============================================================\n var NL = String.fromCharCode(10);\n var raw = '';\n try { raw = (result && result.html) ? String(result.html()) : String(result); } catch (e) { raw = String(result); }\n var pageUrl = String(baseUrl);\n\n function esc(s) {\n return String(s == null ? '' : s)\n .replace(\/&\/g, '&').replace(\/<\/g, '<').replace(\/>\/g, '>')\n .replace(\/\"\/g, '"').replace(\/'\/g, ''');\n }\n function stripTags(s) {\n return String(s == null ? '' : s).replace(\/<[^>]*>\/g, '')\n .replace(\/ \/g, ' ').replace(\/&\/g, '&').replace(\/<\/g, '<')\n .replace(\/>\/g, '>').replace(\/"\/g, '\"').replace(\/'\/g, \"'\")\n .replace(\/\\s+\/g, ' ').trim();\n }\n function pick(re, def) { var m = re.exec(raw); return m ? stripTags(m[1]) : (def || ''); }\n function gv(k, def) {\n var v = '';\n try { v = String(source.get(k)); } catch (e) { v = ''; }\n if (v === 'null' || v === 'undefined' || v === 'NaN' || v === '') v = def || '';\n return v;\n }\n \/\/ ---- 0. 诊断闸门:把三类伪装错误页直接变成可读提示(WS 调试据此定位) ----\n if (!raw || raw.length < 500) {\n return '<div style=\"padding:18px;color:#c33;line-height:1.8\">' +\n '<h3>⚠ 内容页为空(' + raw.length + 'B)<\/h3>' +\n '典型原因:<b>Sad Panda 登录墙(空白页)<\/b> \/ 里站 Cookie 无效。<br>' +\n '请到登录面板粘贴含 <code>igneous<\/code> 的 Cookie,并配置可访问里站的代理。' +\n '<p><a href=\"' + esc(pageUrl) + '\">打开原页面<\/a><\/p><\/div>';\n }\n if (\/temporarily banned|excessive request rate|Your IP address has been banned\/i.test(raw)) {\n return '<div style=\"padding:18px;color:#c33;line-height:1.8\"><h3>⚠ 出口 IP 被站方限频<\/h3>' +\n 'E-Hentai 对自动抓取很敏感(通常封 24h)。请换代理出口或稍后再试,勿频繁刷新。' +\n '<p><a href=\"' + esc(pageUrl) + '\">打开原页面<\/a><\/p><\/div>';\n }\n if (\/Sad Panda|sadpanda|You do not have access|back to the E-Hentai Galleries\/i.test(raw)) {\n return '<div style=\"padding:18px;color:#c33;line-height:1.8\"><h3>⚠ Sad Panda(未登录里站)<\/h3>' +\n 'ExHentai 需要 <code>ipb_member_id \/ ipb_pass_hash \/ igneous<\/code> 三项 Cookie。' +\n '请到登录面板粘贴浏览器里已登录的完整 Cookie。<\/div>';\n }\n if (raw.indexOf('id=\"gdt\"') < 0 && raw.indexOf('class=\"itg') < 0) {\n return '<div style=\"padding:18px;color:#c33;line-height:1.8\"><h3>⚠ 不是画廊详情页(' + raw.length + 'B)<\/h3>' +\n '抓到的页面里没有 <code>#gdt<\/code> 缩略图容器,可能是重定向到论坛登录页或 Cloudflare 挑战页。' +\n '<p><a href=\"' + esc(pageUrl) + '\">打开原页面<\/a><\/p><\/div>';\n }\n\n \/\/ ---- 1. 元数据(全部纯字符串正则,跨 LT\/E 两版 Rhino 无 jsoup 依赖) ----\n var title = pick(\/<h1[^>]*id=[\"']gn[\"'][^>]*>([\\s\\S]*?)<\\\/h1>\/i, '');\n var titleJp = pick(\/<h1[^>]*id=[\"']gj[\"'][^>]*>([\\s\\S]*?)<\\\/h1>\/i, '');\n var category = pick(\/<div[^>]*class=[\"'][^\"']*\\bcs\\b[^\"']*[\"'][^>]*>([\\s\\S]*?)<\\\/div>\/i, '');\n var uploader = pick(\/<div[^>]*id=[\"']gdn[\"'][^>]*>([\\s\\S]*?)<\\\/div>\/i, '');\n var rating = pick(\/<[^>]*id=[\"']rating_label[\"'][^>]*>([\\s\\S]*?)<\\\/[^>]+>\/i, '');\n var favCount = pick(\/<[^>]*id=[\"']favcount[\"'][^>]*>([\\s\\S]*?)<\\\/[^>]+>\/i, '');\n \/\/ ---- 1.5 收藏夹:画廊身份(gid\/token) + 当前收藏态(261113 真实页面结构实锤)----\n \/\/ 已收藏态:<div id=\"gdf\"> 内 <a id=\"favoritelink\">夹名<\/a>;未收藏=文本 \"Add to Favorites\"\n \/\/ 辅助判据:id=\"fav\" 图标 div 的 title=\"夹名\"(未收藏时 fav div 为空、无 title)\n var favGid = '', favTok = '';\n var gm = \/\\\/g\\\/(\\d+)\\\/([0-9a-f]{10})\\\/\/.exec(pageUrl);\n if (gm) { favGid = gm[1]; favTok = gm[2]; }\n var favCur = -1; \/\/ 当前所在收藏夹索引(-1=未收藏;由夹名映射得出)\n var favCurName = ''; \/\/ 当前所在收藏夹名(favoritelink 文本)\n var gdfSeg = '';\n var gdfAt = raw.indexOf('id=\"gdf\"');\n if (gdfAt >= 0) gdfSeg = raw.substring(gdfAt, gdfAt + 900);\n var flm = \/<a[^>]*id=[\"']favoritelink[\"'][^>]*>([\\s\\S]*?)<\\\/a>\/.exec(gdfSeg);\n if (flm) {\n var flTxt = stripTags(flm[1]);\n if (flTxt && !\/Add to Favorites\/i.test(flTxt)) favCurName = flTxt;\n }\n if (!favCurName) {\n var ftm = \/id=[\"']fav[\"'][^>]*>[\\s\\S]{0,260}?title=\"([^\"]*)\"\/.exec(gdfSeg);\n if (ftm) favCurName = stripTags(ftm[1]);\n }\n\n \/\/ 封面:#gd1 区块到 <\/div>,内部 style 里的 url(...);再兜底全页第一张 ehgt 图\n var coverBig = '';\n var gd1 = \/<div[^>]*id=[\"']gd1[\"'][^>]*>([\\s\\S]*?)<\\\/div>\/i.exec(raw);\n var cm = gd1 ? \/url\\((?:[\"']?)([^)\"']+\\.(?:jpg|jpeg|png|webp|gif))(?:[\"']?)\\)\/i.exec(gd1[1]) : null;\n if (!cm) cm = \/url\\((?:[\"']?)(https?:\\\/\\\/[^)\"'\\s]+\\.(?:jpg|jpeg|png|webp|gif))(?:[\"']?)\\)\/i.exec(raw);\n if (cm) { coverBig = cm[1].trim(); }\n\n \/\/ #gdd 表格里 Posted \/ File Size \/ Length(N pages) \/ Favorited\n var posted = '', filesize = '', pagesTxt = '', filecount = 0;\n var gdd = \/<div[^>]*id=[\"']gdd[\"'][^>]*>([\\s\\S]*?)<\\\/div>\/i.exec(raw);\n var gdtHtml = gdd ? gdd[1] : raw;\n var rowRe = \/<tr[^>]*>([\\s\\S]*?)<\\\/tr>\/gi, row;\n while ((row = rowRe.exec(gdtHtml)) !== null) {\n var cells = [], cRe = \/<td[^>]*>([\\s\\S]*?)<\\\/td>\/gi, c;\n while ((c = cRe.exec(row[1])) !== null) cells.push(stripTags(c[1]));\n if (cells.length >= 2) {\n var k = cells[0].replace(\/:$\/, '').toLowerCase(), v = cells[1];\n if (k === 'posted') posted = v;\n else if (k === 'file size') filesize = v;\n else if (k === 'length') {\n pagesTxt = v;\n var fm = \/(\\d+)\\s*pages?\/i.exec(v); if (fm) filecount = Number(fm[1]);\n }\n }\n }\n \/\/ 站点有时把 Length 放在 td.gdt2 而不成行\n if (!filecount) { var fm2 = \/(\\d+)\\s*pages?\/i.exec(stripTags(gdtHtml)); if (fm2) filecount = Number(fm2[1]); }\n\n \/\/ 标签(#taglist 每个 tr:namespace + 若干 a)\n \/\/ ★261114 v5:本段只收集结构化 tagRows,不拼 HTML —— 开关(tagSearch\/tagWatch)、\n \/\/ watchMap、origin 都在更后面才就绪,提前引用会因 var 提升得到 undefined。\n \/\/ 真正渲染在 §3.7(3.6 抓完 mytags 之后)。\n var tagRows = [], tagHtml = '';\n var ts = raw.indexOf('id=\"taglist\"');\n if (ts < 0) ts = raw.indexOf(\"id='taglist'\");\n if (ts >= 0) {\n var te = raw.indexOf('id=\"gdt\"', ts);\n if (te < 0) te = raw.indexOf('id=\"gd5\"', ts);\n if (te < 0) te = Math.min(raw.length, ts + 20000);\n tagHtml = raw.substring(ts, te);\n }\n var trRe = \/<tr[^>]*>([\\s\\S]*?)<\\\/tr>\/gi, tr;\n while ((tr = trRe.exec(tagHtml)) !== null) {\n var tcs = [], tdRe = \/<td[^>]*>([\\s\\S]*?)<\\\/td>\/gi, td;\n while ((td = tdRe.exec(tr[1])) !== null) tcs.push(td[1]);\n if (!tcs.length) continue;\n var ns = stripTags(tcs[0]).replace(\/:$\/, '');\n var picks = [], aRe = \/<a[^>]*href=\"([^\\\"]*\\\/tag\\\/[^\\\"]*)\"[^>]*>([\\s\\S]*?)<\\\/a>\/gi, a;\n while ((a = aRe.exec(tcs.slice(1).join(''))) !== null) {\n var tv = stripTags(a[2]);\n if (!tv) continue;\n var au = String(a[1]).replace(\/&\/g, '&');\n \/\/ tag 全名 = \/tag\/ 后串(+ → 空格),与页面 toggle_tagmenu 的第二参一致;\n \/\/ mytags 的 tagpreview title 用的是同一形态,订阅态比对全靠它。\n var fq = \/\\\/tag\\\/([^?#]*)\/.exec(au);\n var full = fq ? fq[1].replace(\/\\+\/g, ' ') : (ns + ':' + tv);\n picks.push({ t: tv, u: au, f: full });\n }\n if (ns && picks.length && ns.length > 1 && \/^[a-z]+$\/i.test(ns)) tagRows.push({ ns: ns, picks: picks });\n }\n\n \/\/ ---- 2. 缩略图解析(两种形态:gdtm 背景图 \/ gdtl img) ----\n \/\/ 返回 [{thumb, page}],page = \/s\/ 单图页绝对链接\n function hostOf(u) { var m = \/^(https?:\\\/\\\/[^\/]+)\/.exec(u); return m ? m[1] : 'https:\/\/exhentai.org'; }\n var origin = hostOf(pageUrl);\n function abs(u) {\n if (!u) return '';\n if (\/^https?:\\\/\\\/\/i.test(u)) return u;\n if (u.indexOf('\/\/') === 0) return 'https:' + u;\n if (u.indexOf('\/') === 0) return origin + u;\n return origin + '\/' + u;\n }\n function normImg(u) {\n \/\/ 里站缩略图域 s.exhentai.org 归一到公开图床 ehgt.org(Venera normalizeThumbnailHost)\n return String(u).replace(\/https?:\\\/\\\/s\\.exhentai\\.org\/gi, 'https:\/\/ehgt.org')\n .replace(\/https?:\\\/\\\/ehgt\\.org\/gi, 'https:\/\/ehgt.org');\n }\n function parseThumbs(html) {\n var list = [], seen = {};\n \/\/ 真实结构(imgbrd-grabber \/ Venera 实锤):\n \/\/ <div class=\"gdtm\"><div style=\"...background:transparent url(THUMB)...\">\n \/\/ <a href=\"\/s\/HASH\/GID-N\"><img><\/a><\/div><\/div>\n \/\/ 背景图与 <a> 是并列\/父子混合,故分别收集两个数组再按索引对齐。\n var gs = html.indexOf('id=\"gdt\"');\n if (gs < 0) gs = html.indexOf(\"id='gdt'\");\n var block = gs >= 0 ? html.substring(gs) : html;\n \/\/ 截到分页表之前,避免把其它区域的 \/s\/ 链接算进来\n var pe = block.indexOf('class=\"ptb\"');\n if (pe < 0) pe = block.indexOf(\"class='ptb'\");\n if (pe > 0) block = block.substring(0, pe);\n\n \/\/ ① 背景图池 + sprite 偏移(真实站点 gt200 布局:多页共用一张大图,\n \/\/ 靠 background:...url(SPRITE) -0px\/-200px\/-400px ... no-repeat 显示不同区域)\n var bgPool = [], bgX = [], bgY = [], bgW = [], bgH = [];\n \/\/ ★ 真实串:url(SPRITE) -0px 0 no-repeat —— 第二个数字常不带 px,必须兼容\n var bgRe = \/url\\((?:[\"']?)([^)\"']+\\.(?:jpg|jpeg|png|webp|gif))(?:[\"']?)\\)\\s*(-?\\d+)px\\s+(-?\\d+)(?:px)?\/gi, bm;\n while ((bm = bgRe.exec(block)) !== null) {\n bgPool.push(normImg(abs(bm[1].trim())));\n bgX.push(Number(bm[2])); bgY.push(Number(bm[3]));\n }\n \/\/ 尺寸从同一 style 里取(width:200px;height:283px)\n var dimRe = \/style=[\"'][^\"']*width:(\\d+)px;height:(\\d+)px[^\"']*url\\(\/gi, dm2;\n while ((dm2 = dimRe.exec(block)) !== null) { bgW.push(Number(dm2[1])); bgH.push(Number(dm2[2])); }\n \/\/ 兜底:正则没带偏移时按 0\n if (bgPool.length && bgX.length < bgPool.length) {\n while (bgX.length < bgPool.length) { bgX.push(0); bgY.push(0); }\n }\n \/\/ ② img 池:gdtl \/ gt100 \/ gt200 等大缩略图布局\n var imgPool = [], imRe = \/<img[^>]*\\s(?:data-src|src)=[\"']([^\"']+)[\"']\/gi, im;\n while ((im = imRe.exec(block)) !== null) {\n var iu = im[1];\n if (iu.indexOf('data:') === 0 || iu.indexOf('\/g\/') > -1) continue;\n imgPool.push(normImg(abs(iu)));\n }\n \/\/ ③ 链接池:#gdt 内所有 \/s\/ 单图页\n var links = [], aRe = \/<a[^>]*href=[\"']([^\"']*\\\/s\\\/[a-f0-9]{8,12}\\\/[^\"']+)[\"']\/gi, am;\n while ((am = aRe.exec(block)) !== null) {\n var l = abs(am[1].replace(\/&\/g, '&'));\n if (seen[l]) continue;\n seen[l] = 1; links.push(l);\n }\n \/\/ ④ 按索引对齐(bgPool 优先,imgPool 补位),并带上 sprite 裁切参数\n \/\/ ★ 每个缩略图分页有自己独立的一张 sprite 图(合并多页时会有多张),\n \/\/ ⇒ 按 thumb URL 分组,组内用「第 K 个切片 \/ 该组总数 N」算百分比,\n \/\/ 不能用全正文 thumbs.length(跨页会算错)。\n \/\/ ============ sprite 裁切参数(实测结构 e-hentai gt200)============\n \/\/ <a href=\"\/s\/HASH\/ID-N\"><div style=\"width:200px;height:283px;\n \/\/ background:transparent url(SPRITE) -200px 0 no-repeat\"><\/div><\/a>\n \/\/ 单行拼接:X 偏移按 200 递增,Y 恒 0,但每格宽度不同(200\/162\/168\/174…)\n \/\/\n \/\/ 整张 sprite 的真实宽度 = max(偏移 + 该格宽度)。同一 sprite 必须共用这个宽度,\n \/\/ 否则每格各算一个总宽 ⇒ 缩放不一致 ⇒ 越往下越偏。\n var grpEndW = {}, grpSeen = {}, i4;\n for (i4 = 0; i4 < bgPool.length; i4++) {\n var gu = bgPool[i4];\n var ax = Math.abs(bgX[i4] || 0);\n var cwI = (bgW.length > i4 && bgW[i4]) ? bgW[i4] : 200;\n var endX = ax + cwI;\n if (!grpEndW[gu] || endX > grpEndW[gu]) grpEndW[gu] = endX;\n }\n\n for (var n = 0; n < links.length; n++) {\n var t = '', sp = null;\n if (n < bgPool.length) {\n t = bgPool[n];\n var gw = (bgW.length > n && bgW[n]) ? bgW[n] : 200;\n var gh = (bgH.length > n && bgH[n]) ? bgH[n] : 283;\n var ox = Math.abs(bgX[n] || 0);\n var oy = Math.abs(bgY[n] || 0);\n var totW = grpEndW[t] || (ox + gw);\n if (totW < gw) totW = gw;\n sp = { w: gw, h: gh, ox: ox, oy: oy, tw: totW };\n grpSeen[t] = (grpSeen[t] || 0) + 1;\n } else if (n - bgPool.length < imgPool.length) {\n t = imgPool[n - bgPool.length];\n }\n list.push({ link: links[n], thumb: t, sprite: sp });\n }\n\n \/\/ ⑤ 兜底:链接一个都没抓到但存在图片池(站点改版时仍能显示图)\n if (!links.length && imgPool.length) {\n for (var q = 0; q < imgPool.length; q++) list.push({ link: pageUrl, thumb: imgPool[q] });\n }\n return list;\n }\n var thumbs = parseThumbs(raw);\n\n \/\/ ---- 3. 后续缩略图分页:页数完全来自站点自身信号 ----\n \/\/ 判据① table.ptb 里 ?p=N 的最大 N;判据② filecount \/ 本页缩略图数\n \/\/ ★ 页数只跟随站点自身信号(不写死上限):\n \/\/ ① 权威:table.ptb 分页表里 ?p=N 的最大 N(Venera parseThumbnailPage 同源判据)\n \/\/ ② 兜底:ptb 缺失时,用 filecount \/ 本页缩略图数 推算(本页即一整页容量)\n \/\/ ③ 两者都有时以站点 ptb 为准(它列的是真实页数;byCount 会在样本\/截断页上虚高)\n var siteMax = 0;\n var ptb = \/<table[^>]*class=[\"'][^\"']*\\bptb\\b[^\"']*[\"'][^>]*>([\\s\\S]*?)<\\\/table>\/i.exec(raw);\n if (ptb) {\n var p2 = \/[?&]p=(\\d+)\/g, p2m;\n while ((p2m = p2.exec(ptb[1])) !== null) { var n2 = Number(p2m[1]); if (n2 > siteMax) siteMax = n2; }\n }\n var maxP = siteMax;\n if (maxP === 0 && filecount > 0 && thumbs.length > 0) {\n maxP = Math.ceil(filecount \/ thumbs.length) - 1;\n if (maxP < 0) maxP = 0;\n }\n var cleanUrl = pageUrl.split('?')[0].split('#')[0];\n \/\/ ★ 后续页请求必须自带纯净 Cookie 头(与 check.js 同逻辑):\n \/\/ 裸 URL 走 OkHttp+CookieJar 栈会返回 0B 白屏;只发两项让站方重签 igneous。\n \/\/ ★261114 v5.2:绝不硬编码测试账号;没配 ehCookie 时 ckKeep 为空,下面统一走「未配置」提示\n var rawCk = gv('ehCookie', '');\n if (rawCk.indexOf('ipb_member_id=') < 0 || rawCk.indexOf('ipb_pass_hash=') < 0) rawCk = '';\n var ckParts = rawCk.split(';'), ckKeep = [], ckSeen = {}, CKOK = ['ipb_member_id', 'ipb_pass_hash', 'inline_set'];\n \/\/ ★261114 v5.1:分页请求同样显式带合法 igneous(URL 选项 headers 里必须有该键)\n for (var ci = 0; ci < ckParts.length; ci++) {\n var cp = ckParts[ci].trim(), ce = cp.indexOf('=');\n if (ce < 1) continue;\n var ckk = cp.substring(0, ce).trim(), ckv = cp.substring(ce + 1).trim();\n if (CKOK.indexOf(ckk) < 0 || ckSeen[ckk] || ckv === '') continue;\n ckSeen[ckk] = 1; ckKeep.push(ckk + '=' + ckv);\n }\n if (ckKeep.join(';').indexOf('inline_set=') < 0) ckKeep.push('inline_set=dm_l');\n var igP = gv('ehIgneous', '');\n if (igP === 'mystery' || igP === 'null' || igP === 'undefined' || igP === '0') igP = '';\n ckKeep.push('igneous=' + igP); \/\/ 空值占位:覆盖 mystery + 站方重签该账号的值\n var PAGE_HDR = JSON.stringify({ 'User-Agent': 'Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/120.0.0.0 Safari\/537.36', 'Cookie': ckKeep.join('; '), 'Referer': 'https:\/\/exhentai.org\/' });\n \/\/ ★261114 v6.9 hosts直连残留清除:不再读 ehHostsIp \/ 不再带 dnsIp\n \/\/ (对照实验实锤:带IP池dnsIp→banned页;不带→正常。恢复直连功能引入前的正常DNS路径)\nvar OPT = ',{\"headers\":' + PAGE_HDR + '}';\n var more = [], i;\n for (i = 1; i <= maxP; i++) more.push(cleanUrl + '?p=' + i + OPT);\n \/\/ ★ bodies 必须在 if 外初始化:单页画廊(maxP=0)时末尾判据也要读它\n var bodies = [], ok = false;\n\n if (more.length) {\n try {\n \/\/ ajaxAll 走当前订阅源 header(含 proxy\/Cookie);返回元素 .body 是方法\n var rs = java.ajaxAll(more, true);\n \/\/ ★ java.ajaxAll 返回 Java 数组 [L...StrResponse;(只有 .length,没有 .size()),\n \/\/ 部分版本返回 List(有 .size()\/.get(i))—— 双形态兼容,否则整段被 catch 吞掉。\n var n = 0, item;\n if (rs) {\n try { n = rs.length; } catch (eL) { try { n = rs.size(); } catch (eS) { n = 0; } }\n }\n if (n > 0) {\n for (i = 0; i < n; i++) {\n var b = '';\n try { item = rs[i]; } catch (e0) { try { item = rs.get(i); } catch (e1) { item = null; } }\n if (item == null) { bodies.push(''); continue; }\n b = respBody(item);\n \/\/ 判活:必须真的是画廊缩略图页(排除异常堆栈串 \/ 限频页 \/ 空白页)\n if (b.length < 500 || b.indexOf('id=\"gdt\"') < 0 || \/Exception|temporarily banned|Sad Panda\/i.test(b.slice(0, 300))) b = '';\n bodies.push(b);\n }\n ok = bodies.filter(function (x) { return x !== ''; }).length > 0;\n if (!ok) bodies = [];\n }\n } catch (e) { ok = false; bodies = []; }\n if (!ok) {\n \/\/ 降级:串行 java.ajax(同样走 header),逐页判活,异常即停;\n \/\/ 不并发,避免短时密集请求把主请求一起连累进站方临时限频。\n bodies = [];\n for (i = 0; i < more.length; i++) {\n try {\n var one = String(java.ajax(more[i]));\n if (!one || one.length < 500 || one.indexOf('id=\"gdt\"') < 0 ||\n \/Exception|temporarily banned|Sad Panda\/i.test(one.slice(0, 200))) break;\n bodies.push(one);\n } catch (e2) { break; }\n }\n }\n for (i = 0; i < bodies.length; i++) {\n var extra = parseThumbs(bodies[i]);\n for (var j = 0; j < extra.length; j++) {\n var dup = false;\n for (var k = 0; k < thumbs.length; k++) if (thumbs[k].link === extra[j].link) { dup = true; break; }\n if (!dup) thumbs.push(extra[j]);\n }\n }\n }\n\n \/\/ ============ 高清图:服务端并发抓单图页 ============\n \/\/ \/s\/{imgkey}\/{gid}-{page} 页里有 <img id=\"img\" src=\"...\">(中图 1280 宽)。\n \/\/ ★ 域名策略(实测):\n \/\/ 表站 \/s\/ 页:公开可读,但**里站独占的画廊在表站是 404**\n \/\/ 里站 \/s\/ 页:必须带 Cookie 才 200(无 Cookie 是 302 空 body)\n \/\/ ⇒ 两条都试,逐条取成功的那份。不能「全部失败才退」,\n \/\/ 否则里站独占画廊会整片退回缩略图。\n \/\/ ★261109 v3.1:首屏内联高清张数跟随登录面板「🖼 分辨率」档位(ehRes)。\n var RES_TABLE = {\n '0': { hd: 0, gap: 1000, margin: '0px' },\n '1': { hd: 6, gap: 700, margin: '600px' },\n '2': { hd: 15, gap: 450, margin: '1200px' },\n '3': { hd: 25, gap: 350, margin: '1800px' }\n };\n var _resK = gv('ehRes', '1');\n var RES = RES_TABLE[_resK] || RES_TABLE['1'];\n var HD_MAX = RES.hd;\n var hdMap = {};\n var hdDiag = '';\n var hdSample = '';\n var hdLinks = []; \/\/ 原地址(hdMap 的键,渲染时按它查)\n var hdTry = []; \/\/ 表站地址(优先)\n\n function toPublic(u) {\n return String(u)\n .replace(\/^https?:\\\/\\\/exhentai\\.org\/i, 'https:\/\/e-hentai.org')\n .replace(\/^https?:\\\/\\\/s\\.exhentai\\.org\/i, 'https:\/\/e-hentai.org')\n .replace(\/^https?:\\\/\\\/g\\.e-hentai\\.org\/i, 'https:\/\/e-hentai.org');\n }\n\n \/\/ 源 Cookie(只发 ipb_member_id \/ ipb_pass_hash \/ inline_set)\n \/\/ 源 Cookie:只发 ipb_member_id \/ ipb_pass_hash \/ inline_set\n \/\/ ★ 源变量为空时用内置兜底(与 loginCheckJs 同值),\n \/\/ 否则 \/s\/ 页请求不带 Cookie → 里站 302 到首页 → 拿不到 id=\"img\"\n \/\/ ★261114 v5.2:不硬编码账号;没配 ehCookie 时 cookieHdr() 返回空串\n var ckState = 'unused';\n\n function cookieHdr() {\n var raw = '';\n try { raw = String(source.get('ehCookie') || ''); } catch (e0) { raw = ''; }\n if (raw.indexOf('ipb_member_id=') < 0 || raw.indexOf('ipb_pass_hash=') < 0) {\n ckState = 'missing';\n return ''; \/\/ ★ 未配置 ⇒ 不带 Cookie 打 \/s\/(明确提示,绝不用测试账号)\n }\n ckState = 'var';\n var parts = raw.split(';'), keep = [], seen = {}, OK = ['ipb_member_id', 'ipb_pass_hash', 'inline_set'];\n for (var i = 0; i < parts.length; i++) {\n var p = parts[i].trim(), eq = p.indexOf('=');\n if (eq < 1) continue;\n var k = p.substring(0, eq).trim(), v = p.substring(eq + 1).trim();\n if (OK.indexOf(k) < 0 || seen[k] || v === '') continue; \/\/ 忽略 igneous,下面统一补\n seen[k] = 1; keep.push(k + '=' + v);\n }\n if (keep.join(';').indexOf('inline_set=') < 0) keep.push('inline_set=dm_l');\n var ig = '';\n try { ig = String(source.get('ehIgneous') || ''); } catch (eIg) { ig = ''; }\n if (ig === 'mystery' || ig === 'null' || ig === 'undefined' || ig === '0') ig = '';\n keep.push('igneous=' + ig); \/\/ 空值占位\n return keep.join('; ');\n }\n\n function withCookie(u) {\n var ck = cookieHdr();\n if (!ck) return u;\n var h = { 'Cookie': ck, 'Referer': 'https:\/\/exhentai.org\/' };\n return u + ',{\"headers\":' + JSON.stringify(h) + '}';\n }\n\n \/\/ StrResponse 取值:三种形态兼容\n function respBody(r) {\n if (!r) return '';\n var v = '';\n try {\n var b = r.body;\n if (typeof b === 'function') { v = String(b.call(r) || ''); }\n else if (typeof b === 'string') { v = b; }\n else if (b != null) { v = String(b); }\n } catch (e1) { v = ''; }\n if (v && \/^function\\s+\\w+\\s*\\(\/.test(v)) v = '';\n if (v && v.indexOf('java.lang') > -1 && v.length < 200) v = '';\n if (!v) { try { v = String(r.getBody() || ''); } catch (e2) { v = ''; } }\n if (!v) { try { v = String(r.body() || ''); } catch (e3) { v = ''; } }\n if (!v && typeof r === 'string') { v = r; }\n return v;\n }\n\n function hdPick(body) {\n if (!body) return '';\n var s = String(body);\n if (s.length < 200) return '';\n var m = s.match(\/<img[^>]+id=\"img\"[^>]+src=\"([^\"]+)\"\/i)\n || s.match(\/<img[^>]+src=\"([^\"]+)\"[^>]+id=\"img\"\/i)\n || s.match(\/id=\"img\"[^]{0,600}?src=\"([^\"]+)\"\/i);\n return m ? m[1] : '';\n }\n\n for (var hi = 0; hi < thumbs.length && hi < HD_MAX; hi++) {\n if (thumbs[hi] && thumbs[hi].link) {\n hdLinks.push(thumbs[hi].link);\n hdTry.push(toPublic(thumbs[hi].link));\n }\n }\n\n var okN = 0, emptyN = 0, noImgN = 0, mode = '';\n\n \/\/ 跑一批,返回每条结果数组('' = 失败)\n \/\/ 跑一批,返回 {实际请求URL: body} 的映射(不依赖返回顺序)\n \/\/ ★ 关键:ajaxAll 内部是 mapAsync 并发,返回顺序不保证与入参一致。\n \/\/ 盲信数组索引会把第 1 张的图配到第 5 张的键上 —— 表现为「缩略图之间突然插入一张不连贯的图」。\n \/\/ 用 StrResponse.url() 反查真实地址,按 URL 建映射才可靠。\n \/\/ ★ 分批 + 退避:一次只并发 HD_BATCH 个 \/s\/ 页,批间 sleep HD_GAP。\n \/\/ 进页面瞬间把十几页全推出去会被站方判成抓取 —— 表现就是首屏裂图、后续拿空 body。\n var HD_BATCH = 5;\n var HD_GAP = RES.gap;\n\n function nap(ms) {\n try { Packages.java.lang.Thread.sleep(ms); } catch (eN) {}\n }\n\n function runBatch(urls, useCookie) {\n var got = {};\n var canAll = false;\n try { canAll = (typeof java.ajaxAll === 'function'); } catch (e0) { canAll = false; }\n if (!canAll) return got;\n\n var firstBatch = true;\n for (var off = 0; off < urls.length; off += HD_BATCH) {\n if (!firstBatch) nap(HD_GAP);\n firstBatch = false;\n\n var endI = off + HD_BATCH;\n if (endI > urls.length) endI = urls.length;\n var list = [];\n for (var y = off; y < endI; y++) list.push(useCookie ? withCookie(urls[y]) : urls[y]);\n\n var res = null;\n try { res = java.ajaxAll(list, true); mode = mode || 'ajaxAll\/5'; }\n catch (eHd) {\n try { res = java.ajaxAll(list); mode = mode || 'ajaxAll\/5'; }\n catch (e2) { res = null; }\n }\n if (!res) continue;\n collectBatch(res, off, got);\n }\n return got;\n }\n\n function collectBatch(res, off, got) {\n\n var n = 0;\n try { n = res.length; } catch (eL) { try { n = res.size(); } catch (eS) { n = 0; } }\n\n for (var j = 0; j < n; j++) {\n var item = null;\n try { item = res[j]; } catch (e3) { try { item = res.get(j); } catch (e4) { item = null; } }\n if (item == null) continue;\n\n var body = respBody(item);\n\n \/\/ 取响应自带的 url(三种形态兼容)\n var rurl = '';\n try {\n var uu = item.url;\n if (typeof uu === 'function') rurl = String(uu() || '');\n else if (typeof uu === 'string') rurl = uu;\n else if (uu != null) rurl = String(uu);\n } catch (e5) { rurl = ''; }\n if (!rurl) { try { rurl = String(item.getUrl() || ''); } catch (e6) { rurl = ''; } }\n if (rurl && rurl.indexOf('http') < 0) rurl = '';\n\n if (rurl) {\n \/\/ 只取 URL 主体(去掉 ,{...} 选项串)\n rurl = rurl.split(',{')[0];\n got[rurl] = body;\n } else {\n \/\/ 拿不到 url 才退回索引(极少发生)\n got['__IDX__' + (off + j)] = body;\n }\n }\n return got;\n }\n\n if (hdLinks.length) {\n \/\/ ① 表站(公开可读,里站独占的画廊这里会 404)\n var pubBodies = runBatch(hdTry, false);\n var missList = []; \/\/ 表站没拿到的索引\n for (var a = 0; a < hdLinks.length; a++) {\n var pubKey = hdTry[a].split(',{')[0];\n var b1 = pubBodies[pubKey] || pubBodies['__IDX__' + a] || '';\n if (a === 0 && !hdSample && b1 && b1.length > 500) {\n hdSample = String(b1).replace(\/[<>&\"\\n\\r\\t]\/g, ' ').replace(\/\\s+\/g, ' ').slice(0, 40);\n }\n if (!b1) { emptyN++; missList.push(a); continue; }\n var u1 = hdPick(b1);\n if (u1) { hdMap[hdLinks[a]] = u1; okN++; }\n else { noImgN++; missList.push(a); }\n }\n\n \/\/ ② 表站漏掉的 → 里站 + Cookie 补齐(逐条,不整批重试)\n if (missList.length) {\n mode += '+里站补' + missList.length;\n var retryUrls = [], retryIdx = [];\n for (var k = 0; k < missList.length; k++) {\n retryUrls.push(hdLinks[missList[k]]);\n retryIdx.push(missList[k]);\n }\n var exBodies = runBatch(retryUrls, true);\n for (var k2 = 0; k2 < retryUrls.length; k2++) {\n var exKey = retryUrls[k2].split(',{')[0];\n var b2 = exBodies[exKey] || exBodies['__IDX__' + k2] || '';\n if (!b2) continue;\n var u2 = hdPick(b2);\n if (u2) {\n hdMap[hdLinks[retryIdx[k2]]] = u2;\n okN++;\n noImgN = 0; emptyN = Math.max(0, emptyN - 1);\n }\n }\n }\n\n \/\/ ③ 还有漏的 → 逐个 java.ajax(老版本无 ajaxAll)\n var still = 0;\n for (var c = 0; c < hdLinks.length; c++) { if (!hdMap[hdLinks[c]]) still++; }\n if (still > 0 && typeof java.ajax === 'function') {\n mode += '+ajax' + still;\n for (var q = 0; q < hdLinks.length && still > 0; q++) {\n if (hdMap[hdLinks[q]]) continue;\n var one = '';\n try { one = String(java.ajax(withCookie(hdLinks[q]) || '') || ''); } catch (eA) { one = ''; }\n if (!one) { try { one = String(java.ajax(withCookie(hdTry[q]) || '') || ''); } catch (eB) { one = ''; } }\n if (!one) continue;\n var uq = hdPick(one);\n if (uq) { hdMap[hdLinks[q]] = uq; okN++; still--; }\n }\n }\n\n hdDiag = (mode || 'none') + ' ' + okN + '\/' + hdLinks.length +\n ' ck:' + (ckState || '?') +\n (hdSample ? ' {' + hdSample + '}' : '');\n } else {\n hdDiag = '无链接';\n }\n\n \/\/ 剩余待懒加载的链接\n var hdRest = [];\n for (var hr = HD_MAX; hr < thumbs.length; hr++) {\n if (thumbs[hr] && thumbs[hr].link) hdRest.push(thumbs[hr].link);\n }\n\n \/\/ ---- 4. 拼装 HTML ----\n \/\/ ★ 正文列数档位(登录面板「🖼 正文每行 N 图」按钮写入 ehCols)\n \/\/ 默认 '1' = 一行 1 图大图(用户反馈原来 auto-fill 在手机上常变 2 列偏小);'2' = 一行 2 图紧凑。\n \/\/ 订阅源正文环境 source=RssSource 本体,get\/put 直接可用(261030 源码级实锤)。\n \/\/ ---- 3.5 收藏夹名单:面板开关「📁 正文显示收藏夹」开启时才抓取(1 次请求)----\n var showFav = gv('ehFavPanel', '0') === '1';\n var favCats = null, favErr = '';\n \/\/ ★261114 v6.8 名单缓存:favorites.php 每画廊都拉会触发站方 IP+频率风控\n \/\/ (日志实锤:开 3 画廊 = 3 次请求 → temporarily banned 3.5h)。\n \/\/ 名单抓一次缓存 30 分钟(ehFavCache = {t:时间戳, cats:[{n,name,cnt}...]}),\n \/\/ 命中=零请求秒出;收藏操作成功后页内脚本清缓存(下次打开拉新)。\n var FAV_TTL = 30 * 60 * 1000;\n var favCacheRaw = '';\n try { favCacheRaw = String(source.get('ehFavCache') || ''); } catch (eFC) { favCacheRaw = ''; }\n var favCache = null;\n try { if (favCacheRaw) favCache = JSON.parse(favCacheRaw); } catch (eFP) { favCache = null; }\n var favCacheFresh = (favCache && favCache.t && (new Date().getTime() - Number(favCache.t)) < FAV_TTL && favCache.cats && favCache.cats.length >= 0);\n if (showFav && favGid) {\n if (favCacheFresh) {\n favCats = favCache.cats;\n \/\/ 夹名 → 索引映射(favoritelink 只给名字)\n if (favCur < 0 && favCurName) {\n for (var fj = 0; fj < favCats.length; fj++) {\n if (favCats[fj].name === favCurName) { favCur = favCats[fj].n; break; }\n }\n }\n } else {\n try {\n var fb = String(java.ajax(origin + '\/favorites.php' + OPT));\n var fbOK = fb.length > 3000 && fb.indexOf('favcat=') >= 0 &&\n !\/Exception|temporarily banned|Sad Panda\/i.test(fb.slice(0, 300));\n if (fbOK) {\n favCats = [];\n \/\/ 真实结构:<div class=\"fp\" onclick=\"...favcat=N\"...> ①计数div ②fav图标div(title=夹名) ③名字div <\/div>\n \/\/ ★ 段必须取到下一个 fp 为止(首个 <\/div> 只闭合计数 div)\n var fpRe = \/<div class=\"fp\"[^>]*favcat=(\\d)[^>]*>\/g, fpm, fpos = [];\n while ((fpm = fpRe.exec(fb)) !== null) fpos.push({ n: Number(fpm[1]), s: fpm.index, e: fpRe.lastIndex });\n for (var fi = 0; fi < fpos.length; fi++) {\n var segEnd = (fi + 1 < fpos.length) ? fpos[fi + 1].s : (fpos[fi].e + 500);\n var fseg = fb.substring(fpos[fi].e, segEnd);\n var tmm = \/title=\"([^\"]*)\"\/.exec(fseg);\n var cm2 = \/font-weight:bold;[^>]*>([\\d,]+)<\/.exec(fseg);\n var fNm = tmm ? stripTags(tmm[1]) : '';\n favCats.push({ n: fpos[fi].n, name: fNm || ('Favorites ' + fpos[fi].n), cnt: cm2 ? cm2[1] : '' });\n }\n \/\/ 夹名 → 索引映射(favoritelink 只给名字)\n if (favCur < 0 && favCurName) {\n for (var fj = 0; fj < favCats.length; fj++) {\n if (favCats[fj].name === favCurName) { favCur = favCats[fj].n; break; }\n }\n }\n if (!favCats.length) { favCats = null; favErr = '页面里没有 favcat 列表'; }\n \/\/ ★v6.8 抓取成功 → 写缓存(30 分钟内不再请求)\n if (favCats) {\n try { source.put('ehFavCache', JSON.stringify({ t: new Date().getTime(), cats: favCats })); } catch (eFW) { }\n }\n } else {\n favErr = '收藏页获取失败(' + fb.length + 'B' +\n (\/temporarily banned\/i.test(fb) ? ' · IP 限频' : \/Sad Panda\/i.test(fb) ? ' · 登录态无效' :\n fb.length === 0 ? ' · 空白页,检查 Cookie\/igneous 是否为该账号' : ' · 检查代理') + ')';\n }\n } catch (eF) { favErr = '请求异常:' + String(eF).slice(0, 60); }\n }\n }\n if (showFav && !favGid) favErr = '未登录(缺少 gid\/token),收藏夹功能不可用';\n\n \/\/ ---- 3.6 tag 模式:两个开关 + mytags watched 名单(仅订阅开时 1 次请求)----\n var tagSearch = gv('ehTagSearch', '0') === '1';\n var tagWatch = gv('ehTagWatch', '0') === '1';\n var watchMap = {}; \/\/ full tag name -> usertagId(watched 的)\n var tagWarn = '';\n \/\/ ★v6.8 mytags 同样缓存 30 分钟(ehTagCache = {t:..., tags:{name:id,...}})\n var TAG_TTL = 30 * 60 * 1000;\n var tagCacheRaw = '';\n try { tagCacheRaw = String(source.get('ehTagCache') || ''); } catch (eTC) { tagCacheRaw = ''; }\n var tagCache = null;\n try { if (tagCacheRaw) tagCache = JSON.parse(tagCacheRaw); } catch (eTP) { tagCache = null; }\n var tagCacheFresh = (tagCache && tagCache.t && (new Date().getTime() - Number(tagCache.t)) < TAG_TTL && tagCache.tags);\n if (tagWatch && !tagCacheFresh) {\n try {\n var mb = String(java.ajax(origin + '\/mytags' + OPT));\n if (mb.length > 3000 && mb.indexOf('usertag_') >= 0 &&\n !\/Exception|temporarily banned|Sad Panda\/i.test(mb.slice(0, 300))) {\n \/\/ <div id=\"usertag_N\"> 段:tagpreview_N 的 title=\"ns:tag name\" + tagwatch_N checked\n var ur = \/<div id=\"usertag_(\\d+)\">([\\s\\S]*?)(?=<div id=\"usertag_|$)\/g, um;\n while ((um = ur.exec(mb)) !== null) {\n var uid = um[1], useg = um[2];\n if (!\/id=\"tagwatch_\"|id=\"tagwatch_\/.test(useg)) continue;\n var wm = new RegExp('id=\"tagwatch_' + uid + '\"[^>]*checked').exec(useg);\n var pm = new RegExp('id=\"tagpreview_' + uid + '\"[^>]*title=\"([^\"]*)\"').exec(useg);\n if (wm && pm) watchMap[stripTags(pm[1])] = uid;\n }\n \/\/ ★ 兜底:某些版本段被截断时,用全页正则按 id 逐条找\n if (!Object.keys(watchMap).length) {\n var wr2 = \/id=\"tagwatch_(\\d+)\"[^>]*checked\/g, w2;\n while ((w2 = wr2.exec(mb)) !== null) {\n var p2 = new RegExp('id=\"tagpreview_' + w2[1] + '\"[^>]*title=\"([^\"]*)\"').exec(mb);\n if (p2) watchMap[stripTags(p2[1])] = w2[1];\n }\n }\n \/\/ ★v6.8 成功 → 写缓存\n try { source.put('ehTagCache', JSON.stringify({ t: new Date().getTime(), tags: watchMap })); } catch (eTW) { }\n } else {\n tagWarn = '· mytags 未取到(' + mb.length + 'B,订阅态可能不准)';\n }\n } catch (eT) { tagWarn = '· mytags 异常:' + String(eT).slice(0, 50); }\n } else if (tagWatch && tagCacheFresh) {\n \/\/ ★缓存命中:watchMap 从缓存还原(零请求)\n var tk = tagCache.tags;\n for (var tkn in tk) watchMap[tkn] = tk[tkn];\n }\n\n \/\/ ---- 3.7 tag 渲染(开关\/watchMap\/origin 此时全部就绪)----\n var tags = [];\n for (var tri = 0; tri < tagRows.length; tri++) {\n var row = tagRows[tri];\n tags.push('<div class=\"ehTagRow\"><b>' + esc(row.ns) + '<\/b>:' +\n row.picks.map(function (p) {\n var su = origin + '\/?f_search=' + encodeURIComponent(p.f);\n var label = tagSearch\n ? '<a class=\"ehTag\" href=\"' + esc(su) + '\">' + esc(p.t) + '<\/a>'\n : '<span class=\"ehTag\">' + esc(p.t) + '<\/span>';\n var chip = '';\n if (tagWatch) {\n var w = watchMap[p.f];\n chip = '<button class=\"ehTagW' + (w ? ' on' : '') + '\" data-full=\"' + esc(p.f) +\n '\" data-id=\"' + (w ? esc(String(w)) : '') + '\">' +\n (w ? '✓' : '+') + '<\/button>';\n }\n return label + chip;\n }).join('') + '<\/div>');\n }\n\n var cols = gv('ehCols', '1');\n if (cols !== '2') cols = '1';\n var gridCols = (cols === '1') ? '1fr' : 'repeat(2,1fr)';\n\n var css = [\n 'body{margin:0;padding:0;background:#1b1b1f;color:#e8e8ea;font:15px\/1.7 system-ui,-apple-system,\"Noto Sans CJK SC\",sans-serif}',\n '.ehCard{background:#26262c;border:1px solid #3a3a44;border-radius:10px;padding:14px;margin-bottom:14px}',\n '.ehTitle{font-size:20px;font-weight:700;color:#fff;word-break:break-word;margin:0 0 4px}',\n '.ehJp{font-size:15px;color:#b9b9c6;margin:0 0 10px;word-break:break-word}',\n '.ehMeta{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px 14px;margin-top:10px}',\n '.ehMeta div{font-size:13px;color:#c9c9d4}.ehMeta b{color:#8ab4f8;font-weight:600}',\n '.ehTagRow{margin:5px 0;font-size:13px;color:#c9c9d4}.ehTagRow b{color:#f0a0a0}',\n '.ehTag{display:inline-block;background:#34343f;border:1px solid #454554;border-radius:5px;padding:1px 7px;margin:2px 4px 2px 0;font-size:12px;color:#dcdce6;text-decoration:none}',\n '.ehTag:active{background:#4a5a7a;border-color:#6a7fa8}',\n '.ehCover{max-width:100%;height:auto;display:block;margin:0 auto 12px;border-radius:8px}',\n '.ehGrid{display:block;line-height:0;font-size:0}',\n '.ehGrid a{display:block;margin:0;padding:0;background:transparent;border-radius:0;overflow:visible;text-decoration:none}',\n '.ehGrid a>img{width:100%;height:auto;display:block;margin:0;border:0;padding:0;background:transparent}',\n '.ehGrid a>div{display:block;margin:0;padding:0}',\n '.ehGrid> a >div{width:100%;display:block;margin:0;padding:0;background:transparent}',\n '.ehGrid span{display:none}',\n '.ehBar{font-size:13px;color:#a9a9b8;margin:10px 0}.ehBar a{color:#8ab4f8}',\n '.ehWarn{background:#4a2b2b;border:1px solid #8a4444;padding:10px;border-radius:8px;color:#ffd7d7;font-size:13px;margin-top:12px}', '.ehFavRow{margin-top:12px;padding-top:10px;border-top:1px dashed #3a3a44}',\n '.ehFavHead{font-size:14px;font-weight:700;color:#f0a0a0;margin-bottom:6px}',\n '.ehFavErr{font-size:12px;color:#e8a0a0;background:#3a2626;border-radius:6px;padding:3px 8px;margin-left:6px}',\n '.ehFavStat{font-size:13px;color:#c9c9d4;margin:4px 0 8px}',\n '.ehFavB{display:inline-block;background:#34343f;border:1px solid #454554;border-radius:6px;color:#dcdce6;padding:5px 10px;margin:3px 6px 3px 0;font-size:13px;cursor:pointer;text-align:left}',\n '.ehFavB i{font-style:normal;color:#a9a9b8;font-size:11px;margin-left:5px}',\n '.ehFavB b{font-weight:600;color:#7ee787;margin-left:5px}',\n '.ehFavB.cur{background:#2d4a2d;border-color:#7ee787;color:#c8f0c8}',\n '.ehFavB.busy{opacity:0.5;pointer-events:none}',\n '.ehFavDel{background:#4a2b2b;border-color:#8a4444;color:#ffd7d7}', '.ehTagW{display:inline-block;background:#2d4a2d;border:1px solid #4a7c4a;border-radius:4px;color:#c8f0c8;padding:0 6px;margin:0 8px 2px -2px;font-size:11px;cursor:pointer;vertical-align:middle}',\n '.ehTagW.on{background:#3a5f3a;border-color:#7ee787}',\n '.ehTagW.busy{opacity:0.5;pointer-events:none}',\n '.ehTagMode{font-size:12px;color:#a9a9b8;margin:6px 0 2px}',\n '.ehHud{position:fixed;right:8px;bottom:8px;z-index:60;background:transparent;color:#fff;font:12px\/1.4 monospace;text-shadow:0 1px 3px rgba(0,0,0,.9),0 0 6px rgba(0,0,0,.8);pointer-events:none;padding:2px 4px}'\n ].join('');\n\n var h = '<!doctype html><html><head><meta charset=\"utf-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no\">' +\n '<style>' + css + '<\/style><\/head><body>';\n var totalPages = filecount || thumbs.length;\n var hdCount0 = 0;\n for (var hk in hdMap) { hdCount0++; }\n \/\/ 正常不打扰:只显示页码。高清全失败时才提示一句。\n var hudHint = '';\n if (!hdCount0 && hdLinks.length) hudHint = ' ·无高清';\n h += '<div class=\"ehHud\" id=\"ehHud\" data-diag=\"' + esc(hdDiag) + '\" data-hd=\"' + hdCount0 + '\">' +\n '1 \/ ' + totalPages + hudHint + '<\/div>';\n h += '<div class=\"ehCard\">';\n h += '<h1 class=\"ehTitle\">' + esc(title || '(未取到标题)') + '<\/h1>';\n if (titleJp) h += '<div class=\"ehJp\">' + esc(titleJp) + '<\/div>';\n\n h += '<div class=\"ehMeta\">';\n function mi(k, v) { if (v) h += '<div><b>' + esc(k) + ':<\/b>' + esc(v) + '<\/div>'; }\n mi('分类', category); mi('上传者', uploader); mi('发布时间', posted);\n mi('页数', pagesTxt || (filecount ? filecount + ' pages' : '')); mi('大小', filesize);\n mi('评分', rating); mi('收藏', favCount);\n h += '<\/div>';\n if (tags.length) {\n if (tagSearch || tagWatch) {\n h += '<div class=\"ehTagMode\">🏷 标签:' +\n (tagSearch ? '<b>点名字搜索<\/b>' : '名字不可点') + ' · ' +\n (tagWatch ? '<b>+\/✓ 订阅(再点取消)<\/b>' : '订阅未开') +\n (tagWarn ? ' <span class=\"ehFavErr\">' + esc(tagWarn) + '<\/span>' : '') + '<\/div>';\n }\n h += '<div style=\"margin-top:6px\">' + tags.join('') + '<\/div>';\n }\n\n var hdCount = 0;\n for (var hc in hdMap) { hdCount++; }\n\n h += '<\/div>';\n\n \/\/ ---- 4.5 收藏夹 UI(正文信息下方:状态行 + 10 个夹子 chip + 移出按钮)----\n if (showFav) {\n h += '<div class=\"ehFavRow\">';\n h += '<div class=\"ehFavHead\">📁 收藏夹' +\n (favErr ? ' <span class=\"ehFavErr\">' + esc(favErr) + '<\/span>' : '') + '<\/div>';\n if (favCats) {\n var favHas = (favCur >= 0 || favCurName);\n h += '<div class=\"ehFavStat\" id=\"ehFavStat\">' + (favHas\n ? '⭐ 已收藏:' + esc(favCurName || ('夹 ' + favCur)) + '(再点该夹或 ➖ 移出)'\n : '☆ 未收藏 · 点击下方收藏夹加入') + '<\/div>';\n for (var fi2 = 0; fi2 < favCats.length; fi2++) {\n var fc = favCats[fi2];\n var isCur = (favCur === fc.n);\n h += '<button class=\"ehFavB' + (isCur ? ' cur' : '') + '\" data-i=\"' + fc.n + '\">' +\n esc(fc.name) + (fc.cnt ? '<i>' + esc(fc.cnt) + '<\/i>' : '') +\n (isCur ? '<b>✓<\/b>' : '') + '<\/button>';\n }\n if (favHas) h += '<button class=\"ehFavB ehFavDel\" id=\"ehFavDel\">➖ 移出收藏夹<\/button>';\n }\n h += '<\/div>';\n }\n\n if (thumbs.length) {\n h += '<div class=\"ehGrid\">';\n for (i = 0; i < thumbs.length; i++) {\n var t = thumbs[i];\n var media;\n var hdUrl = hdMap[t.link] || '';\n if (hdUrl) {\n media = '<img src=\"' + esc(hdUrl) + '\" alt=\"' + (i + 1) +\n '\" loading=\"lazy\" decoding=\"async\">';\n } else if (t.sprite && t.thumb) {\n var s = t.sprite;\n var gw = s.w || 200, gh = s.h || 283;\n var totW = s.tw || (gw + (s.ox || 0));\n if (totW < gw) totW = gw;\n \/\/ 容器按本格宽高比撑开\n var padTop = (gh * 100 \/ gw).toFixed(4) + '%';\n \/\/ 背景宽度:整张 sprite 相对本格的放大倍数(同一 sprite 各格差值仅来自 gw)\n var bgWid = (totW * 100 \/ gw).toFixed(4) + '%';\n \/\/ 背景横向位置。CSS 百分比语义:实际位移 = (容器宽 - 图宽) * p,\n \/\/ 容器窄、图宽时该值为负,所以要左移 ox 必须给「正的 p」:\n \/\/ p = ox \/ (图宽 - 容器宽) = ox \/ (totW - gw)\n \/\/ 写成负号会让图右移 ⇒ 越往下越偏。\n var bgPos = (totW > gw) ? ((s.ox || 0) * 100 \/ (totW - gw)).toFixed(4) + '%' : '0%';\n media = '<div style=\"width:100%;padding-top:' + padTop + ';' +\n 'background-image:url(' + esc(t.thumb) + ');' +\n 'background-repeat:no-repeat;' +\n 'background-size:' + bgWid + ' auto;' +\n 'background-position:' + bgPos + ' 0;' +\n 'background-color:transparent\"><\/div>';\n } else if (t.thumb) {\n media = '<img src=\"' + esc(t.thumb) + '\" alt=\"' + (i + 1) +\n '\" loading=\"lazy\" decoding=\"async\">';\n } else {\n media = '<img src=\"https:\/\/ehgt.org\/g\/t.png\" alt=\"' + (i + 1) +\n '\" loading=\"lazy\" decoding=\"async\">';\n }\n \/\/ ★ 首屏已内联高清的格子直接标 data-hd=1,HUD 的 loading 只留给还在排队的格子。\n var hdFlag = hdUrl ? ' data-hd=\"1\"' : ' data-need=\"1\"';\n h += '<a data-orig=\"' + esc(t.link) + '\"' + hdFlag + '>' + media + '<\/a>';\n }\n h += '<\/div>';\n } else {\n h += '<div class=\"ehWarn\">⚠ 未解析到缩略图。可能原因:①站点改版(#gdt 形态变化)' +\n '②当前是 Compact 之外的布局(面板已强制 inline_set=dm_l,请清理排序缓存后重试)' +\n '③代理未生效导致只拿到半页。<\/div>';\n }\n if (more.length > 0 && bodies.length > 0 && bodies.length < more.length) {\n h += '<div class=\"ehWarn\">⚠ 后续 ' + more.length + ' 个缩略图分页只成功取回 ' + bodies.length +\n ' 页(代理\/限频\/超时)。已显示能取到的全部;配好代理后重新打开本页即可补全。<\/div>';\n }\n if (tagWatch && tags.length) {\n \/\/ 页内 tag 订阅脚本:点 +\/✓ → java.ajax POST \/mytags(add \/ mass 删除)→ 即时改 DOM。\n \/\/ ★ add 与 delete 都返回 302 空响应(站方 POST→GET 重定向)⇒ 不用响应体判成功:\n \/\/ java.ajax 拿到 302 跟随后的 \/mytags 页面即为成功(失败则是异常堆栈串\/空)。\n \/\/ ★ 动态值一律 JSON.stringify(261114 ReferenceError 铁律);options 用对象 stringify。\n var tagLines = [\n '(function(){',\n 'var O=__ORIGIN__,HDR=__HDR__;',\n 'var Q=String.fromCharCode(34);',\n 'function call(u){var w=\"\";try{var r=java.ajax(u);if(typeof r===\"string\"){w=r;}else{try{w=String(r.body());}catch(e1){try{w=String(r);}catch(e2){w=\"\";}}}}catch(e){w=\"\";}return w;}',\n 'function hip(){return \"\";}',\n 'function H(){var h=null;try{h=JSON.parse(HDR);}catch(e0){h=null;}if(!h){h={Cookie:\"\",Referer:O+\"\/\"};}h[\"Content-Type\"]=\"application\/x-www-form-urlencoded\";return h;}',\n 'function post(body){var _hp=hip();var _o={method:\"POST\",body:body,headers:H()};var opt=\",\"+JSON.stringify(_o);var w=call(O+\"\/mytags\"+opt);return (w&&w.indexOf(\"usertag_\")>=0)?w:\"\";}',\n 'function findId(w,full){var re=\/id=\"tagpreview_([0-9]+)\"[^>]*title=\"([^\"]*)\"\/g,m;while((m=re.exec(w))!==null){if(m[2]===full)return m[1];}return \"\";}',\n 'function onClick(ev){',\n ' if(ev&&ev.stopPropagation)ev.stopPropagation();',\n ' if(ev&&ev.preventDefault)ev.preventDefault();',\n ' var el=this;if(el.className.indexOf(\"busy\")>=0)return;',\n ' var full=el.getAttribute(\"data-full\")||\"\",id=el.getAttribute(\"data-id\")||\"\";',\n ' if(!full)return;',\n ' var adding=!id;',\n ' el.className+=\" busy\";el.textContent=\"…\";',\n ' var body=adding?(\"usertag_action=add&tagname_new=\"+encodeURIComponent(full)+\"&tagwatch_new=on&tagcolor_new=&tagweight_new=10&usertag_target=0\")',\n ' :(\"usertag_action=mass&usertag_target=0&modify_usertags[]=\"+encodeURIComponent(id));',\n ' var resp=post(body),ok=!!resp;',\n ' el.className=el.className.split(\" busy\").join(\"\");',\n ' if(ok&&adding){',\n ' var nid=findId(resp,full);',\n ' if(nid)el.setAttribute(\"data-id\",nid);',\n ' try{source.put(\"ehTagCache\",\"\");}catch(eCT){}',\n ' el.className=\"ehTagW on\";el.textContent=\"✓\";',\n ' java.toast(\"✅ 已订阅标签:\"+full+(nid?\"\":\"(退出重进可刷新订阅态)\"));',\n ' }else if(ok){',\n ' el.removeAttribute(\"data-id\");el.className=\"ehTagW\";el.textContent=\"+\";',\n ' java.toast(\"✅ 已取消订阅:\"+full);',\n ' }else{',\n ' el.textContent=adding?\"+\":\"✓\";',\n ' java.toast(\"❌ 订阅操作失败 ⇒ 检查登录态\/网络\");',\n ' }',\n '}',\n 'var bs=document.querySelectorAll(\".ehTagW\");',\n 'for(var k=0;k<bs.length;k++){bs[k].addEventListener(\"click\",onClick);}',\n '})();'\n ].join(String.fromCharCode(10))\n .replace('__ORIGIN__', JSON.stringify(origin))\n .replace('__HDR__', JSON.stringify(PAGE_HDR));\n h += '<script>' + tagLines + '<\/script>';\n }\n\n if (showFav && favCats && favGid) {\n \/\/ 页内收藏夹脚本:点击夹子 → java.ajax POST gallerypopups.php → 成功后即时改 DOM。\n \/\/ ★ 零反斜杠纪律(261109 实锤):不用正则字面量、不手拼引号 —— options 用\n \/\/ JSON.parse(PAGE_HDR) 得对象 → 加 Content-Type → JSON.stringify 整个 options。\n \/\/ ★ java.ajax 失败不抛异常而是返回异常堆栈串(code 200)⇒ 判活查响应含 window.opener\n \/\/ (261113 沙盒真机实锤的成功签名;失败串里不会出现该签名)。\n var fcLines = [\n '(function(){',\n 'var G=__GID__,T=__TOK__,O=__ORIGIN__,HDR=__HDR__;',\n 'var stat=document.getElementById(\"ehFavStat\");',\n 'function say(m){if(stat)stat.textContent=m;}',\n 'var hasJava=false,canAjax=false;try{hasJava=(typeof java!==\"undefined\"&&java!=null);if(hasJava){canAjax=(typeof java.ajax===\"function\");}}catch(eJ){}',\n 'function call(u){var w=\"\";if(!canAjax)return \"NOBRIDGE\";try{var r=java.ajax(u);if(typeof r===\"string\"){w=r;}else{try{w=String(r.body());}catch(e1){try{w=String(r);}catch(e2){w=\"\";}}}}catch(e){w=\"\";}return w;}',\n 'function hip(){return \"\";}',\n 'function ok(w){return w&&w.indexOf(\"window.opener\")>=0;}',\n 'var busy=false;',\n 'function all(){return document.querySelectorAll(\".ehFavB\");}',\n 'function cur(){var a=all();for(var i=0;i<a.length;i++)if(a[i].className.indexOf(\"cur\")>=0)return a[i];return null;}',\n 'function clr(el){if(!el)return;var ms=el.querySelectorAll(\"b\");for(var i=0;i<ms.length;i++)el.removeChild(ms[i]);el.className=\"ehFavB\";}',\n 'function onClick(){',\n ' if(busy)return;',\n ' var el=this;',\n ' if(el.id===\"ehFavDel\"){act(el,false,\"\");return;}',\n ' var idx=el.getAttribute(\"data-i\");',\n ' if(idx===null||idx===\"\")return;',\n ' act(el,!(cur()===el),idx);',\n '}',\n 'function act(el,adding,idx){',\n ' busy=true;el.className+=\" busy\";',\n 'if(!canAjax){say(\"❌ 此环境无 java 桥(E 版 WebView 限制),无法收藏\");return;}',\n ' say(adding?\"⏳ 正在加入收藏夹…\":\"⏳ 正在移出收藏夹…\");',\n 'try{',\n ' var body=adding?(\"favcat=\"+idx+\"&favnote=&apply=Add+to+Favorites&update=1\")',\n ' :\"favcat=favdel&favnote=&apply=Apply+Changes&update=1\";',\n ' var H=null;try{H=JSON.parse(HDR);}catch(e0){H=null;}',\n ' if(!H){H={Cookie:\"\",Referer:O+\"\/\"};}',\n ' H[\"Content-Type\"]=\"application\/x-www-form-urlencoded\";',\n ' var _hp=hip();var _o={method:\"POST\",body:body,headers:H};',\n ' var opt=\",\"+JSON.stringify(_o);',\n ' var u=O+\"\/gallerypopups.php?gid=\"+G+\"&t=\"+T+\"&act=addfav\",w=\"\";',\n ' w=call(u+opt);',\n ' el.className=el.className.split(\" busy\").join(\"\");',\n ' if(ok(w)){',\n ' var s2=cur();if(s2&&s2!==el)clr(s2);',\n ' clr(el);',\n ' try{source.put(\"ehFavCache\",\"\");}catch(eCF){}',\n ' if(adding){el.className=\"ehFavB cur\";var b=document.createElement(\"b\");b.textContent=\"✓已入\";el.appendChild(b);say(\"✅ 已加入收藏夹(站内即时生效)\");}',\n ' else{say(\"✅ 已移出收藏夹\");}',\n ' }else{',\n ' say(\"❌ 操作失败(\"+(w?w.length+\"B 异常响应\":\"空响应\")+\")⇒ 检查尝试登录态\/网络后重试\");',\n ' }',\n ' }catch(eA){say(\"❌ 异常:\"+String(eA).slice(0,80));}',\n ' finally{busy=false;}',\n '}',\n 'var bs=all();',\n 'for(var k=0;k<bs.length;k++){bs[k].addEventListener(\"click\",onClick);}',\n '})();',\n ].join(String.fromCharCode(10))\n .replace('__GID__', JSON.stringify(favGid))\n .replace('__TOK__', JSON.stringify(favTok))\n .replace('__ORIGIN__', JSON.stringify(origin))\n .replace('__HDR__', JSON.stringify(PAGE_HDR));\n h += '<script>' + fcLines + '<\/script>';\n }\n if (true) {\n var lazyCode = [\"(function(){\", \"var Q=String.fromCharCode(34);\", \"var NL=String.fromCharCode(10);\", \"var pend=__PEND__;\", \"var hud=document.getElementById('ehHud');\", \"var TOTAL=__TOTAL__;\", \"var CK=__CK__;\", \"var REF=__REF__;\", \"var GAP=__GAP__;\", \"var MARGIN=__MARGIN__;\", \"function upd(t){if(hud){hud.textContent=t;}}\", \"if(!pend||!pend.length){upd('1 \/ '+TOTAL);return;}\", \"\/\/ 261109 v3.1 纯 JS 视口懒加载:零反斜杠,适配官方版 WebJsExtensions 与 E 版无桥。\", \"var hasJava=false,canAjax=false,canGet=false;\", \"try{hasJava=(typeof java!=='undefined'&&java!=null);if(hasJava){canAjax=(typeof java.ajax==='function');canGet=(typeof java.get==='function');}}catch(e0){}\", \"var AS=[].slice.call(document.querySelectorAll('.ehGrid a[data-need]'));\", \"var MAP={};\", \"for(var i=0;i<AS.length;i++){\", \" var u0=AS[i].getAttribute('data-orig');\", \" for(var j=0;j<pend.length;j++){if(pend[j]===u0){MAP[i]={a:AS[i],u:u0};break;}}\", \"}\", \"var PENDN=0;for(var pn in MAP){PENDN++;}\", \"if(!PENDN){upd('1 \/ '+TOTAL);return;}\", \"\/\/ 从 \/s\/ 页 HTML 抽 <img id=img src=...>:正则里不含引号,用 Q 拼双引号后 new RegExp\", \"function pickSrc(h){\", \" if(!h)return '';\", \" var s=String(h);\", \" if(s.length<200)return '';\", \" var pat='<img[^>]+id='+Q+'img'+Q+'[^>]+src='+Q+'([^'+Q+']+)'+Q;\", \" var m=null;\", \" try{m=new RegExp(pat,'i').exec(s);}catch(e1){m=null;}\", \" if(!m){pat='<img[^>]+src='+Q+'([^'+Q+']+)'+Q+'[^>]+id='+Q+'img'+Q;try{m=new RegExp(pat,'i').exec(s);}catch(e2){m=null;}}\", \" return m?m[1]:'';\", \"}\", \"function fetchOne(u){\", \" var src='';\", \" if(canGet&&CK){try{src=pickSrc(java.get(u,JSON.stringify({Cookie:CK,Referer:REF||u}),15000));}catch(a){}}\", \" if(!src&&canAjax){try{src=pickSrc(java.ajax(u));}catch(b){}}\", \" if(!src&&canAjax&&CK){\", \" var hip='';\",\n \" var opt=','+String.fromCharCode(123)+Q+'headers'+Q+':'+JSON.stringify({Cookie:CK,Referer:REF||u})+''+String.fromCharCode(125);\", \" try{src=pickSrc(java.ajax(u+opt));}catch(c){}\", \" }\", \" return src;\", \"}\", \"function toPublic(u){\", \" \/\/ 里站 \/s\/ 换表站:用字符串替换,不用正则反斜杠\", \" return String(u).split('exhentai.org').join('e-hentai.org');\", \"}\", \"function fetchAny(u){var s=fetchOne(toPublic(u));if(!s)s=fetchOne(u);return s;}\", \"function swap(a,src){\", \" var old=a.querySelector('img')||a.querySelector('div');\", \" if(!old||!old.parentNode)return;\", \" var im=new Image();\", \" im.loading='lazy';im.decoding='async';\", \" im.style.cssText='width:100%;height:auto;display:block;margin:0;padding:0;background:transparent';\", \" im.onload=function(){a.setAttribute('data-hd','1');};\", \" im.src=src;\", \" old.parentNode.replaceChild(im,old);\", \" a.setAttribute('data-hd','1');\", \"}\", \"var Q2=[],queued={},done={},running=false,okN=0,failN=0;\", \"function enqueue(idx){if(queued[idx]||done[idx])return;var it=MAP[idx];if(!it)return;queued[idx]=1;Q2.push(idx);kick();}\", \"function kick(){if(!running){running=true;setTimeout(pump,0);}}\", \"function pump(){\", \" if(!Q2.length){running=false;return;}\", \" var idx=Q2.shift();\", \" if(done[idx]){setTimeout(pump,0);return;}\", \" var it=MAP[idx];\", \" var src='';\", \" if(hasJava&&(canAjax||canGet)){src=fetchAny(it.u);}\", \" if(src){swap(it.a,src);done[idx]=1;okN++;}else{failN++;delete queued[idx];}\", \" updateHud(idx);\", \" setTimeout(pump,GAP);\", \"}\", \"function topVisible(){\", \" var vh=window.innerHeight||document.documentElement.clientHeight||0;\", \" for(var q=0;q<AS.length;q++){var r=null;try{r=AS[q].getBoundingClientRect();}catch(e){}if(r&&r.bottom>0&&r.top<vh)return q;}\", \" return -1;\", \"}\", \"function updateHud(last){\", \" var t=topVisible();if(t<0)t=(last>=0?last:0);\", \" var tag=AS[t]&&AS[t].getAttribute('data-hd')==='1'?'':' -加载中';\", \" upd((t+1)+' \/ '+TOTAL+tag+' ('+okN+'\/'+PENDN+')');\", \"}\", \"var io=null;\", \"if(typeof IntersectionObserver==='function'){\", \" io=new IntersectionObserver(function(entries){\", \" for(var e=0;e<entries.length;e++){\", \" if(!entries[e].isIntersecting)continue;\", \" var idx=Number(entries[e].target.getAttribute('data-idx'));\", \" enqueue(idx);\", \" io.unobserve(entries[e].target);\", \" }\", \" },{root:null,rootMargin:MARGIN,threshold:0.01});\", \" for(var k=0;k<AS.length;k++){AS[k].setAttribute('data-idx',String(k));io.observe(AS[k]);}\", \"}else{\", \" var lastT=-1,timer=null;\", \" function onScroll(){\", \" if(timer)return;\", \" timer=setTimeout(function(){\", \" timer=null;var t=topVisible();\", \" if(t>=0&&t!==lastT){lastT=t;for(var z=t;z<t+3;z++)enqueue(z);}\", \" updateHud(t);\", \" },120);\", \" }\", \" if(window.addEventListener){window.addEventListener('scroll',onScroll,false);window.addEventListener('resize',onScroll,false);}\", \" onScroll();\", \"}\", \"if(!hasJava){upd('1 \/ '+TOTAL+' 原生懒加载(E版)');}\", \"})();\"].join(String.fromCharCode(10))\n .replace('__PEND__', JSON.stringify(hdRest))\n .replace('__TOTAL__', String(totalPages))\n .replace('__CK__', JSON.stringify(cookieHdr()))\n .replace('__REF__', JSON.stringify(origin))\n .replace('__GAP__', String(RES.gap))\n .replace('__MARGIN__', JSON.stringify(RES.margin));\n h += '<script>' + lazyCode + '<\/script>';\n }\n h += '<\/body><\/html>';\n return h;\n})()",
"ruleImage": "@js:(function(){\n function norm(u){\n var s = String(u == null ? '' : u).replace(\/&\/g, '&').trim();\n s = s.replace(\/^https?:\\\/\\\/s\\.exhentai\\.org\/i, 'https:\/\/ehgt.org');\n s = s.replace(\/^https?:\\\/\\\/ehgt\\.org\/i, 'https:\/\/ehgt.org');\n return s;\n }\n function ok(u){\n var s = norm(u);\n if (!s || s.indexOf('http') !== 0) return '';\n if (s.indexOf('\/g\/') > -1) return '';\n if (s.indexOf('ehgt.org\/') > -1) return s;\n if (s.indexOf('hath.network') > -1) return s;\n return '';\n }\n try{\n var i = result.select('.glthumb img');\n for (var a = 0; a < i.size(); a++) {\n var v = ok(i.get(a).attr('data-src')) || ok(i.get(a).attr('src'));\n if (v) return v;\n }\n var j = result.select('img');\n for (var b = 0; b < j.size(); b++) {\n var v2 = ok(j.get(b).attr('data-src')) || ok(j.get(b).attr('src'));\n if (v2) return v2;\n }\n var dv = result.select('div');\n for (var c = 0; c < dv.size(); c++) {\n var st = String(dv.get(c).attr('style') || '');\n if (st.indexOf('url(') < 0) continue;\n var mm = st.match(\/url\\(([^)]+)\\)\/);\n if (mm) {\n var v3 = ok(String(mm[1]).replace(\/[\"']\/g, ''));\n if (v3) return v3;\n }\n }\n }catch(e){}\n return '';\n})()",
"ruleLink": "@js:(function(){try{var a=result.select('a[href*=\/g\/]');if(a.size()>0){var h=a.get(0).attr('href');if(h)return h;}}catch(x){}return '';})()",
"ruleNextPage": "@js:(function(){\n \/\/ ★261109 v3 分类页分页完善(全部基于 261109 真机实测,不写死页码上限):\n \/\/ ① 首页 \/ f_cats 分类 \/ watched:站点用 #unext「?next=GID」游标,href 自带筛选参数\n \/\/ (实测 https:\/\/exhentai.org\/?f_cats=1021&next=4315094、\/watched?next=...),\n \/\/ 必须原样跟随(丢参数会翻回全部分类 = 旧版隐性 bug)。\n \/\/ ② popular:站点实测单页给全(81 个画廊),无 #unext \/ 无 ptb \/ ?p= 无效\n \/\/ ⇒ 返回空让「空列表自然终止」,不伪造下一页(不写死上限,跟随站点自身信号)。\n \/\/ ③ & 必须解码;只认本站域名;与本页 URL 相同则停(防回环)。\n \/\/ ④ 用户偏好:翻页终止一律用站点自身的 next 链接,站点以后加数据自动继续。\n var s = '';\n try { s = String(result); } catch (e) { return ''; }\n if (!s || s.length < 100) return '';\n \/\/ 伪装错误页直接停,避免把限频\/白屏页当\"还有下一页\"\n if (\/temporarily banned|excessive request|Sad Panda|You do not have access\/i.test(s.slice(0, 400))) return '';\n\n var href = '';\n var m = s.match(\/<a[^>]*id=[\"']unext[\"'][^>]*href=[\"']([^\"']+)[\"']\/i)\n || s.match(\/<a[^>]*href=[\"']([^\"']*[?&]next=\\d+[^\"']*)[\"'][^>]*id=[\"']unext[\"']\/i);\n if (m && m[1]) href = m[1];\n if (!href) {\n \/\/ 兜底:任意 next=游标链接(排除 javascript: 与站外)\n var m2 = s.match(\/href=[\"']([^\"']*[?&]next=\\d+[^\"']+)[\"']\/i);\n if (m2 && m2[1]) href = m2[1];\n }\n \/\/ ★261114 v6.5:排行榜 toplist.php 用 table.ptt 页码分页(p 从 0 起),与 #unext 游标体系不同。\n \/\/ 旧规则只认 unext ⇒ 排行榜永远停在第 1 页(「排行榜不显示」的规则级真因之一)。\n \/\/ 跟随站点自身分页信号,不写死页码上限。\n if (!href) {\n var base0 = '';\n try { base0 = String(baseUrl || ''); } catch (eB0) { base0 = ''; }\n var ci0 = base0.indexOf(','); \/\/ baseUrl 可能带 URL options 后缀,剥离\n if (ci0 > 0) base0 = base0.substring(0, ci0);\n var mm = s.match(\/<(?:div|table)[^>]*class=[\"'][^\"']*\\bptt\\b[^\"']*[\"'][^>]*>([\\s\\S]*?)<\\\/(?:div|table)>\/i);\n var pg = mm ? mm[1] : s;\n var reAll = \/<a[^>]*href=[\"']([^\"']*(?:toplist\\.php)?[^\"']*[?&]p=\\d+[^\"']*)[\"'][^>]*>([^<]*)<\\\/a>\/gi;\n var ra, best = null, bestP = -1, curP = -1;\n try {\n var cm = base0.match(\/[?&](?:p|page)=(\\d+)\/);\n if (cm) curP = parseInt(cm[1], 10);\n } catch (eCp) { curP = -1; }\n while ((ra = reAll.exec(pg)) !== null) {\n var lp = String(ra[2] || '').trim();\n var pn = ra[1].match(\/[?&]p=(\\d+)\/);\n if (!pn) continue;\n var pi = parseInt(pn[1], 10);\n if (lp === '>' || lp === '>' || lp === '»') { if (pi > bestP && pi !== curP) { best = ra[1]; bestP = pi; } }\n else if (pi === curP + 1 && pi > bestP) { best = ra[1]; bestP = pi; }\n }\n if (!best) {\n var re2 = \/<a[^>]*href=[\"']([^\"']*[?&](?:p|page)=(\\d+)[^\"']*)[\"']\/gi;\n var rb;\n while ((rb = re2.exec(pg)) !== null) {\n var pi2 = parseInt(rb[2], 10);\n if (pi2 === curP + 1) { best = rb[1]; break; }\n }\n }\n if (best) {\n best = best.replace(\/&\/g, '&').replace(\/&\/g, '&').trim();\n if (best.indexOf('javascript:') === 0) best = '';\n \/\/ 相对链接归一化:\n \/\/ ① '?p=2' 纯查询串 ⇒ 基于当前 URL 的路径+其余参数重建(保留 tl=,替换 p=)\n \/\/ ② '\/toplist.php?...' \/ 'toplist.php?...' 路径相对 ⇒ 接到 origin\n if (best.indexOf('?') === 0 && best.indexOf('http') !== 0) {\n var bi = base0.indexOf('?');\n var pathPart = bi >= 0 ? base0.substring(0, bi) : base0;\n var qPart = bi >= 0 ? base0.substring(bi + 1) : '';\n var pClean = best.replace(\/^\\?\/, '');\n var pm2 = pClean.match(\/(?:^|&)(p|page)=(\\d+)\/);\n var pKey = pm2 ? pm2[1] : 'p';\n var kv = qPart.split('&'), keep4 = [];\n for (var q4 = 0; q4 < kv.length; q4++) {\n var k4 = kv[q4].split('=')[0];\n if (k4 && k4 !== 'p' && k4 !== 'page' && k4 !== 'next') keep4.push(kv[q4]);\n }\n if (pm2) keep4.push(pKey + '=' + pm2[2]);\n best = pathPart + '?' + keep4.join('&');\n } else if (best.indexOf('http') !== 0) {\n var ho = '';\n try { ho = base0.match(\/^https?:\\\/\\\/[^\\\/]+\/i)[0]; } catch (eHr) { ho = 'https:\/\/e-hentai.org'; }\n best = ho + (best.charAt(0) === '\/' ? best : '\/' + best);\n }\n href = best;\n }\n }\n if (!href) return ''; \/\/ popular \/ 末页:站点自身无 next ⇒ 终止\n href = href.replace(\/&\/g, '&').replace(\/&\/g, '&').trim();\n if (href.indexOf('javascript:') === 0) return '';\n \/\/ 只放行本站两个域(相对路径也允许,交给 getAbsoluteURL)\n if (\/^https?:\/i.test(href) && !\/(exhentai\\.org|e-hentai\\.org)\/i.test(href)) return '';\n \/\/ 防回环:与本页完全相同的 next 不重复请求\n var cur = '';\n try { cur = String(baseUrl); } catch (e2) { cur = ''; }\n if (cur && href === cur) return '';\n return href;\n})()",
"rulePubDate": "@js:(function(){try{var p=result.select('div[id^=posted_]');if(p.size()>0){var t=String(p.get(0).text()).trim();if(t)return t;}var g3=result.select('td.gl3e > div');for(var pd=0;pd<g3.size();pd++){var t2=String(g3.get(pd).text()).trim();if(\/^\\d{4}-\\d{2}-\\d{2}\/.test(t2))return t2;}}catch(x){}return '';})()",
"ruleTitle": "@js:(function(){try{var e=result.select('.glink');if(e.size()>0){var t=String(e.get(0).text()).trim();if(t)return t;}var a=result.select('a[href*=\/g\/]');if(a.size()>0){var t2=String(a.get(0).text()).trim();if(t2)return t2;}}catch(x){}return '';})()",
"searchUrl": "{{(function(){\nfunction gv(k,def){var v='';try{v=String(source.get(k))}catch(e){v=''}if(v==='null'||v==='undefined'||v==='NaN'||v==='')v=def||'';return v}\nvar d=gv('ehHost','https:\/\/exhentai.org').replace(\/\\\/+$\/,'');\nif(d.indexOf('http')!==0)d='https:\/\/'+d;\nvar p=(page&&page>1)?page:1;\nreturn d+'\/?f_search='+encodeURIComponent(key)+'&page='+p})()}}",
"shouldOverrideUrlLoading": "(function () {\n var u = String(url);\n var canOpen = false, canToast = false;\n try { canOpen = (typeof java !== 'undefined' && typeof java.open === 'function'); } catch (e) { canOpen = false; }\n try { canToast = (typeof java !== 'undefined' && typeof java.toast === 'function'); } catch (e2) { canToast = false; }\n\n \/\/ 站内标签\/搜索页 -> 源内列表\n var isTag = \/\\\/(tag|search)\\\/\/.test(u) || \/[?&]f_search=\/.test(u) || \/[?&]f_(tags|cat|parody|char|artist|circle|fem|mal|lang|misc|rej|pages|exp|doujin)\/.test(u);\n if (isTag) {\n if (canOpen) { java.open('sort', u, 'TAG'); return true; }\n if (canToast) { java.toast('此阅读版本不支持源内打开标签'); }\n return true;\n }\n\n \/\/ 其它站外链接:拦下\n if (\/^https?:\/i.test(u)) {\n if (!\/(exhentai\\.org|e-hentai\\.org|ehgt\\.org|hath\\.network)\/.test(u)) {\n if (canToast) { java.toast('已拦截外部跳转'); }\n return true;\n }\n }\n return false;\n})()",
"showWebLog": false,
"singleUrl": false,
"sortUrl": "@js:\n(function () {\n \/\/ ★261114 v6:\n \/\/ ①「⭐ 关注」= OR 实时搜索(~\"tag$\" 前缀 + 空格分隔,EHWiki 权威语法 + 26-tag 实测)。\n \/\/ \/watched 站点页有站方缓存延迟(新订阅 tag ~1h 内不出,实测订阅后立即访问仍空态),\n \/\/ 实时搜索无此问题。★URL 行内联自包含 JS(AnalyzeUrl URL 环境无 loginUrl 函数作用域,\n \/\/ 不能调 EH_WATCHED_Q);抓不到标签时回退 \/watched 站点页。\n \/\/ 内联逻辑与 loginUrl 的 EH_WATCHED_Q 重复,改动须两处同步。\n \/\/ ②「🏆 排行榜」= e-hentai.org\/toplist.php?tl=N&p=0(里站没有 toplist.php,nginx 404 实测;\n \/\/ Venera ehentai.js 实锤永远走表站;里站账号 SSO 同一 Cookie 对表站有效)。\n \/\/ 分页 p 从 0 开始(table.ptt),与 ?next= 游标体系不同。\n \/\/ ③ 不写死页码上限:关注搜索用 unext 游标自然终止;排行榜跟随站点 ptt 分页(200 页站点上限)。\n function gv(k, def) {\n var v = '';\n try { v = String(source.get(k)); } catch (e) { v = ''; }\n if (v === 'null' || v === 'undefined' || v === 'NaN' || v === '') v = def || '';\n return v;\n }\n var d = gv('ehHost', 'https:\/\/exhentai.org').replace(\/\\\/+$\/, '');\n if (d.indexOf('http') !== 0) d = 'https:\/\/' + d;\n var C = [\n ['🆕 最新(全部)', ''],\n ['📈 热门', 'popular'],\n ['⭐ 关注', 'watched'],\n ['📚 同人志 Doujinshi', 'f_cats=1021'],\n ['📖 漫画 Manga', 'f_cats=1019'],\n ['🎨 画师CG Artist CG', 'f_cats=1015'],\n ['🎮 游戏CG Game CG', 'f_cats=1007'],\n ['🖼 图集 Image Set', 'f_cats=991'],\n ['👗 Cosplay', 'f_cats=959'],\n ['🌏 亚洲 Asian Porn', 'f_cats=895'],\n ['🚫 全年龄 Non-H', 'f_cats=767'],\n ['🌍 欧美 Western', 'f_cats=511'],\n ['📦 杂项 Misc', 'f_cats=1022']\n ];\n var JS_TOKEN = '@js' + ':'; \/\/ 防大块内字面量 @js: 造成解析歧义(廉价保险)\n var out = [], i, name, q, url;\n for (i = 0; i < C.length; i++) {\n name = C[i][0]; q = C[i][1];\n if (q === '') {\n url = d + '\/';\n } else if (q === 'watched') {\n url = JS_TOKEN + JSON.stringify((function(){\nfunction g(k){var v='';try{v=String(source.get(k))}catch(e){v=''}if(v==='null'||v==='undefined'||v==='NaN')v='';return v}\nvar UA='Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/120.0.0.0 Safari\/537.36';\nvar d=g('ehHost').replace(\/\\\/+$\/,'');\nif(d.indexOf('http')!==0)d='https:\/\/exhentai.org';\nvar raw=g('ehCookie');\nvar ig=g('ehIgneous')||'';\nif(ig==='mystery'||ig==='undefined'||ig==='null'||ig==='0')ig='';\nvar CK='igneous=';\nif(raw.indexOf('ipb_member_id=')>=0&&raw.indexOf('ipb_pass_hash=')>=0){\nvar parts=raw.split(';'),keep=[],seen={},OK=['ipb_member_id','ipb_pass_hash','inline_set'];\nfor(var i=0;i<parts.length;i++){var p=parts[i].trim(),eq=p.indexOf('=');if(eq<1)continue;var k=p.substring(0,eq).trim(),v=p.substring(eq+1).trim();if(OK.indexOf(k)<0||seen[k]||v==='')continue;seen[k]=1;keep.push(k+'='+v)}\nif(keep.join(';').indexOf('inline_set=')<0)keep.push('inline_set=dm_l');\nkeep.push('igneous='+ig);\nCK=keep.join('; ')}\ntry{\nvar H0={'User-Agent':UA,'Cookie':CK,'Referer':d+'\/'};\nvar mb=String(java.ajax(d+'\/mytags,'+JSON.stringify(H0)));\nif(!mb||mb.length<3000||mb.indexOf('usertag_')<0||\/temporarily banned|excessive request|Sad Panda\/i.test(mb.slice(0,300)))return d+'\/watched';\nvar tags=[],re=\/id=\"usertag_(\\d+)\">([\\s\\S]*?)(?=<div id=\"usertag_|id=\"mytag\"|$)\/g,m;\nwhile((m=re.exec(mb))!==null){var uid=m[1],seg=m[2];var wm=new RegExp('id=\"tagwatch_'+uid+'\"[^>]*checked').exec(seg);var pm=new RegExp('id=\"tagpreview_'+uid+'\"[^>]*title=\"([^\"]*)\"').exec(seg);if(wm&&pm)tags.push(String(pm[1]))}\nif(!tags.length){var wr=\/id=\"tagwatch_(\\d+)\"[^>]*checked\/g,w;while((w=wr.exec(mb))!==null){var p2=new RegExp('id=\"tagpreview_'+w[1]+'\"[^>]*title=\"([^\"]*)\"').exec(mb);if(p2)tags.push(String(p2[1]))}}\nif(!tags.length)return d+'\/watched';\nif(tags.length>26)tags=tags.slice(0,26);\nvar q=[];\nfor(var j=0;j<tags.length;j++)q.push('~\"'+tags[j]+'$\"');\nreturn d+'\/?f_search='+encodeURIComponent(q.join(' '));\n}catch(eX){return d+'\/watched'}\n})());\n } else if (q === 'tl15' || q === 'tl13' || q === 'tl12' || q === 'tl11') {\n var tl = q.substring(2);\n \/\/ ★261114 v6.3:排行榜只在表站。可配 ehProxyTop(独立代理,没配则跟随 ehProxy);\n \/\/ 表站被封时换节点即可(封禁按 IP 记,~24h 自动解封)。\n \/\/ 表站 Cookie 只发两项+inline_set(igneous 是里站专属,不对表站发)。\n var px = gv('ehProxyTop', '') || gv('ehProxy', '');\n var eh = d.replace('exhentai', 'e-hentai');\n var u0 = eh + '\/toplist.php?tl=' + tl + '&p=0';\n var ck = 'ipb_member_id=';\n var raw2 = gv('ehCookie', '');\n if (raw2.indexOf('ipb_member_id=') >= 0 && raw2.indexOf('ipb_pass_hash=') >= 0) {\n var parts2 = raw2.split(';'), keep2 = [], seen2 = {};\n var OK2 = ['ipb_member_id', 'ipb_pass_hash', 'inline_set'];\n for (var x2 = 0; x2 < parts2.length; x2++) {\n var p2 = parts2[x2].trim(), eq2 = p2.indexOf('=');\n if (eq2 < 1) continue;\n var k2 = p2.substring(0, eq2).trim(), v2 = p2.substring(eq2 + 1).trim();\n if (OK2.indexOf(k2) < 0 || seen2[k2] || v2 === '') continue;\n seen2[k2] = 1; keep2.push(k2 + '=' + v2);\n }\n if (keep2.join(';').indexOf('inline_set=') < 0) keep2.push('inline_set=dm_l');\n ck = keep2.join('; ');\n } else { ck = ''; }\n var opt = { headers: { 'User-Agent': 'Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/120.0.0.0 Safari\/537.36', 'Referer': eh + '\/' } };\n if (ck) opt.headers.Cookie = ck;\n if (px) opt.proxy = px;\n url = u0 + ',' + JSON.stringify(opt);\n } else if (q === 'popular') {\n url = d + '\/' + q;\n } else {\n url = d + '\/?' + q;\n }\n out.push(name + '::' + url);\n }\n return out.join('\\n');\n})()\n",
"sourceComment": "需要梯子和登录后使用,\n推荐cookis登录不用人机验证。\n增加分辨率选择,选择后点导入账号。\n增加添加收藏夹功能,搜索tag,订阅tag功能\n\nExHentai \/ E-Hentai 图库订阅源。站点 5 种列表布局(gltc\/gltm\/glte\/gl1t\/gt100-200)全兼容;正文自拼纯净 HTML(元数据+标签+缩略图 grid),后续缩略图分页页数完全由站点 #gdd 的 N pages 与 table.ptb 的 ?p=N 推导(不写死上限)。★261107 v2 修复:①面板输入框值由 handleButtonClick 绑成 result(不是 source.put),现已从 result 读并落库;②只发 ipb_member_id+ipb_pass_hash 两项,带旧 igneous 或 CookieJar merge 的 igneous=mystery 会导致 0B 白屏,不带则站方自动重签有效 igneous(30天);③enabledCookieJar=false 避免库内键污染;④loginCheckJs 用 java.ajax 纯净栈重取 HTML 并 new StrResponse 替换 OkHttp 栈的 0B 响应。白屏时点面板「🩺 一键自愈」。里站需有效账号 Cookie;VPN 出口若被站方拉黑需换节点。★登录\/注册:①账号密码登录(论坛 POST act=Login&CODE=01,遇 Cloudflare 盾自动转内置浏览器);②📝 注册新账号(官方论坛 act=Reg&CODE=00,需过CF+填 Login Username\/Display Name\/Password\/Email\/Security Code;EHWiki 明确免费、不发验证邮件、立即可用,但每人仅允许1个活跃账号);③🔑 找回用户名\/密码;④浏览器完成后「🔄 读取登录 Cookie」把 ipb_member_id\/pass_hash 写回并显式复制到 exhentai.org 域(Cookie 按二级域归并,forums\/e-hentai 与 exhentai 不共享);⑤保留原 Cookie 粘贴通道。★正文缩略图默认「一行 1 图」大图,登录面板「🖼 正文图片布局」按钮可切「一行 2 图」紧凑;站点 sprite 缩略图用百分比 background-size\/position 自适应,两档都精确裁到对应页。兼容 LegadoTeam 与 legado-E。\n★261109 v3:①登录面板新增「🖼 分辨率选择」select 下拉 4档(省流250px缩略图\/自动780x\/高清1280x\/超清全量,LegadoTeam与legado-E双版本通用;⚙同步站点分辨率会POST \/uconfig.php 写账号 xr\/ts,需主动点);②正文缩略图原生 loading=lazy + 真·IntersectionObserver 视口懒加载,首屏高清张数与 \/s\/ 串行间隔跟随档位,绝不整章并发轰炸(防站方限频);③分类页分页跟随站点 #unext 游标并保留 f_cats 筛选参数、解码 &、防回环,popular 单页自然终止(不写死上限);④UA 由 header <js> 统一注入并补 Accept-Language\/Sec-Fetch 指纹;⑤新增「⚡ 测试各线路延时」把 ms 写进线路按钮 + 「✅ 检查登录状态」;搜索不主动探测频率。\n★261114 v5.1:修复「每次打开订阅源要清 Cookie 重登」——根因 enabledCookieJar=true 让 CookieJar 把 igneous=mystery 回写库内,每次请求自动补上导致 0B 白屏;改为 false + 四处 Cookie 显式带合法 igneous(源变量 ehIgneous,面板「🔄 刷新 igneous」可更新)。\n★261114 v5.2:移除硬编码测试账号凭据\/igneous,改为 igneous= 空值占位(覆盖库内 mystery + 站方按账号重签);未配 Cookie 明确提示;面板加「🩺 登录态一键诊断」。\n★261114 v5.3:修复登录面板按钮显示 null(源码 rowUiBuilder 只对长度3..19的单引号 viewName 当静态文本,超长串被当JS求值失败→null);22行已改短字面量\/删除,11个动态函数加非空兜底。\n★261114 v5.4:登录面板全部 viewName 静态化(11个动态行改短字面量 u16len≤19,100%避开「JS求值→空→显示null」分支);动态状态由点击 action 的 Toast 显示。\n★261114 v5.3c:修复按钮显示null真因=EH_refreshIgneous三元表达式多余右括号→loginUrl整体编译失败→所有动态viewName求值全挂;另7个动态函数整函数try\/catch(||兜底只救falsy不救异常);11个动态viewName保留,App真机探针11\/11返回非空。\n★261114 v6.4 全站hosts直连:源变量 ehHostsIp(空=关闭),列表\/搜索\/loginCheckJs\/正文分页\/收藏夹\/标签订阅\/排行榜全通道注入URL options的dnsIp键(真机反组127.0.0.1→ConnectException实锤生效);登录面板=你的原版未动。\n★26114 v6.7 精简:撤除 hostsIP 直连全套(UI 4行\/EH_hosts* 4函数\/预填池\/按域分流\/ehIg 三级)——用户路径 TLS 握手阶段即按 SNI 阻断(HTTP日志 SSLHandshakeException 实锴),Legado OkHttp 必发 SNI 无法复刻 EhViewer 无 SNI 握手,hostsIP 只解 DNS 层对此路径无效。保留规则级修复:①排行榜 table.ptt 页码分页②排行榜扩展布局日期兜底③searchUrl IIFE 语法 bug 修复(v6.4 就有)。连接走 VPN\/代理(②代理地址),ehHostsIp 变量无 UI 入口恒空=天然关闭。\n★26114 v6.7.1:按用户要求删除 4 个排行榜入口(🏆日榜\/月榜\/年榜\/总榜)——其走表站 toplist.php,用户路径下表站不通(关 VPN 时 DNS\/SNI 全挂,开 VPN 时入口 IP 也常被表站封禁)。分类现保留 13 项(最新\/热门\/关注 + 10 个里站分类)。ruleNextPage 的 ptt 分页分支与 ehProxyTop 变量保畘(无入口即不产生行为)。\n★261114 v6.8 收藏夹\/订阅名单缓存:favorites.php+mytags 每画廊自动连发引发站方 IP+频率风控(日志实锤:开3画廊=3次请求→temporarily banned 3.5h;EhViewer 正常=其按需单次请求)。修法:①名单抓一次缓存30分钟(ehFavCache\/ehTagCache,命中零请求)②banned\/失败绝不缓存③点+\/✓操作成功后页内脚本清缓存(下次打开拉新)④收藏夹开关默认关(header 一次性迁移重置老用户已存的1,ehFavReset68 标记)。请求频率降到人类水平。\n★261114 v6.9:彻底移除 hosts IP 直连残留(8处 dnsIp 注入 + 缓存残留 IP 池一次性清除)。对照实验实锤:带残留IP池dnsIp→favorites.php 返回 239B temporarily banned 共享IP限频页(LT版收藏夹「IP限制」的真凶);不带→96921B 正常收藏页。E版缓存干净故一直正常。如未来站点再次全面被墙需要 IP 直连,须整链路重新设计(含缓存值治理),不可只加回读取逻辑。",
"sourceGroup": "🔞 福利",
"sourceIcon": "https:\/\/exhentai.org\/favicon.ico",
"sourceName": "🅴ExHentai",
"sourceUrl": "https:\/\/exhentai.org\/",
"style": "body{margin:0;padding:0;overflow-x:hidden;max-width:100%}img{display:block;max-width:100%!important;height:auto}a{color:#8ab4f8}",
"type": 0,
"variableComment": "ehHost=站点根域(默认 https:\/\/exhentai.org);ehProxy=代理地址(可空,走VPN全局);ehCookie=里站Cookie(至少 ipb_member_id+ipb_pass_hash);ehCookieInput=粘贴框;ehUser\/ehPass=论坛账号密码(pwdLogin 用,遇 CF 转内置浏览器);ehCols=正文每行图片数(1=单列大图默认 \/ 2=双列紧凑);★发送时只保留两项+inline_set=dm_l,igneous 由站方重新签发;ehRes=分辨率档位(0省流\/1自动默认\/2高清\/3超清);ehXR=站点uconfig回读的xr值;ehCols=正文每行图片数;ehPingEx\/ehPingTable\/ehPingText=线路测速ms;ehPub=发布页\/自定义线路输入框;ehFavPanel=正文显示收藏夹开关(v6.8默认关);ehFavCache=收藏夹名单缓存{t,cats}(TTL30min,操作成功即清);ehTagCache=mytags订阅缓存{t,tags}(同);ehTagSearch=点正文tag名字搜索对应tag(1=开);ehTagWatch=正文tag旁显示+\/✓订阅watched标签(1=开,POST \/mytags add\/mass);ehIgneous=该账号自己的有效 igneous(面板「🔄刷新igneous」写入;为空时用空值占位让站方重签,绝不硬编码\/绝不存 mystery);enabledCookieJar=false 防止 CookieJar 把 igneous=mystery 回写库内导致每次白屏;★本源不内置任何账号:ehCookie 未配置时明确提示粘贴你自己的 Cookie;★ehHostsIp 已在 v6.9 废弃清除(IP池直连=共享IP被站方限频,收藏夹「IP限制」真凶),如键仍有值会被 header 迁移自动清空"
}