Compare commits

...
2 Commits
Author SHA1 Message Date
chenxin 089ce55d22 ✨ feat(bookSource): 重写聚合书源,支持小说/听书/漫画/短剧与段评
- 朴素相对 URL 模式:searchUrl/bookUrl/tocUrl/chapterUrl 全链路,跨规则状态 java.put 传递,弃用 data:;base64 链(真实阅读端解码语义不符)
- 四模式分流:小说/听书/漫画/短剧;搜索支持 x:/t:/m:/d: 前缀;听书优先视频原声回退 TTS,短剧走 CENC 解密代理直链自动播放,空正文回退漫画图片(兼容 data 与 data.images 两种响应结构)
- 段评气泡:对接 /content?review=keep 注入的 <comment> 标记,纯 JS base64(规避部分版本 API 差异),点击打开 /review_html 评论页
- 发现页:设置选择按钮两两一行、男频/女频频道切换(分类减半)、搜索框按类型自动加前缀、分类一行 3 个统一排版
- 短剧搜索 video_data 字段归一化(series_id→book_id、title→book_name、cover→thumb_url、演员→author)
- 兼容性:设置存取走 cache(全规则环境可用),BaseUrl 三级兜底(source→book.origin→baseUrl),段评正则兼容 <p idx> 属性,无登录页依赖
2026-10-10 02:09:29 +08:00
chenxin a652049ba4 🔧 fix(deploy): nginx 模板自带 443 配置,防止 --sync-nginx 覆盖证书段
- 原模板仅 80,443 依赖 certbot --nginx 注入,--sync-nginx 整文件覆盖会抹掉注入配置导致证书回退
- 检测 /etc/letsencrypt/live/<域名>/ 证书存在时自动启用 443,80 仅留 ACME 验证与 301 跳转
- certbot 改用 certonly --webroot,不再改写 nginx 配置;反代 location 抽取为 80/443 共享段
2026-10-10 02:09:29 +08:00
2 changed files with 99 additions and 43 deletions

No files matched your search

+36 -34
View File
@@ -1,8 +1,8 @@
[ [
{ {
"bookSourceComment": "fanqie-go 自建服务代理版 v1\n签名(Gorgon/Argus/Ladon)、设备注册、正文解密、缓存全部由服务端完成,本地零计算、零设备注册。\n接口:/search /detail /catalog /content /category-landing。\n正文参数固定 text_mode=plain&review_mode=remove,不依赖服务器配置。\n发现页:男频/女频 × 精选/标签 四组共 280 个入口(已剔除空分类与推荐流降级标签)。\n依赖服务器可用,服务失效则书源整体失效,无本地回退。", "bookSourceComment": "fanqie-go 聚合书源:一个源覆盖 小说/听书/漫画/短剧。\n\n【设置入口】发现页顶部选择按钮:模式/音色/清晰度/段评/图片/频道,点选即生效;",
"bookSourceGroup": "🍅 番茄", "bookSourceGroup": "🍅 番茄",
"bookSourceName": "🍅 番茄小说", "bookSourceName": "🍅 番茄聚合",
"bookSourceType": 0, "bookSourceType": 0,
"bookSourceUrl": "https://fq.20010117.xyz", "bookSourceUrl": "https://fq.20010117.xyz",
"customOrder": 0, "customOrder": 0,
@@ -10,36 +10,15 @@
"enabledCookieJar": false, "enabledCookieJar": false,
"enabledExplore": true, "enabledExplore": true,
"header": "{\"User-Agent\": \"Mozilla/5.0 (Linux; Android 13; 2210132C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.5845.92 Mobile Safari/537.36\"}", "header": "{\"User-Agent\": \"Mozilla/5.0 (Linux; Android 13; 2210132C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.5845.92 Mobile Safari/537.36\"}",
"lastUpdateTime": 1791554265601, "jsLib": "// ============ 源变量配置(照光遇模式:defaultConfig 兜底,全 try-catch) ============\nconst defaultConfig = {\n 模式: \"小说\",\n 音色: \"0\",\n 清晰度: \"max\",\n 段评: \"开\",\n 图片: \"保留\"\n};\n\nconst fqVarsKey = 'fq_vars';\n\nfunction getVariable(k) {\n if (k == undefined) k = \"\";\n var parsed = {};\n try {\n var raw = String(cache.get(fqVarsKey) || '');\n if (raw) parsed = JSON.parse(raw);\n } catch (e) {}\n if (k == \"\") return parsed;\n var value = parsed[k];\n if (value == undefined) value = defaultConfig[k];\n return value != undefined ? value : \"\";\n}\n\nfunction setVariable(k, v, t) {\n if (t == undefined) t = true;\n var parsed = {};\n try {\n var raw = String(cache.get(fqVarsKey) || '');\n if (raw) parsed = JSON.parse(raw);\n } catch (e) {}\n parsed[k] = v;\n try { cache.put(fqVarsKey, JSON.stringify(parsed)); } catch (e) {}\n if (t) java.toast('\\n设置 ' + k + ' 为 ' + v);\n}\n\n// 发现页筛选按钮回调:存变量并刷新发现页\nfunction show(m, t) {\n setVariable(t, m, false);\n java.toast('\\n已设置' + t + ':' + m + '\\n已在书架的书切换模式后需刷新目录');\n try { java.refreshExplore(); } catch (e) {}\n}\n\nfunction createFilter(title, chars, defaultVal, paramKey, size, viewName) {\n if (viewName == undefined) viewName = \"'请选择类型'\";\n return {\n title: title,\n type: \"select\",\n chars: chars,\n default: defaultVal,\n \"viewName\": viewName,\n action: `show(infoMap['${title}'],'${paramKey}')`,\n style: {\n layout_flexGrow: 1,\n layout_flexBasisPercent: size\n }\n };\n}\n\n// ============ 基础工具 ============\nfunction BaseUrl() {\n var u = '';\n try {\n if (typeof source != 'undefined' && source && source.getKey) u = String(source.getKey());\n } catch (e) {}\n if (!u) {\n try {\n if (typeof book != 'undefined' && book && book.origin) u = String(book.origin);\n } catch (e) {}\n }\n if (!u) {\n try {\n if (typeof baseUrl != 'undefined') {\n var m = String(baseUrl).match(/^https?:\\/\\/[^\\/]+/);\n if (m) u = m[0];\n }\n } catch (e) {}\n }\n return u.replace(/\\/+$/, '');\n}\n\nfunction request(url) {\n let urla = url;\n if (!url.includes('http')) urla = this.BaseUrl() + url;\n return java.ajax(urla);\n}\n\nfunction parseJsonSafely(str, defaultValue) {\n if (defaultValue == null) defaultValue = null;\n try {\n return JSON.parse(str);\n } catch (e) {\n return defaultValue;\n }\n}\n\nfunction isIOS() {\n try {\n java.deviceID();\n return true;\n } catch (e) {\n return false;\n }\n}\n\nfunction isMod() {\n try {\n if (typeof java.reLoginView == 'function') return true;\n new Packages.io.legato.kazusa.utils.TimeoutCancellationException('');\n return true;\n } catch (e) {\n return false;\n }\n}\n\n// 纯 JS base64(UTF-8),规避部分阅读版本 base64 编码 API 不可用\nfunction b64encode(str) {\n var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n var bytes = [];\n str = String(str);\n for (var i = 0; i < str.length; i++) {\n var c = str.charCodeAt(i);\n if (c < 0x80) {\n bytes.push(c);\n } else if (c < 0x800) {\n bytes.push(0xC0 | (c >> 6), 0x80 | (c & 63));\n } else {\n bytes.push(0xE0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));\n }\n }\n var out = '';\n for (var j = 0; j < bytes.length; j += 3) {\n var b1 = bytes[j];\n var b2 = j + 1 < bytes.length ? bytes[j + 1] : undefined;\n var b3 = j + 2 < bytes.length ? bytes[j + 2] : undefined;\n out += chars[b1 >> 2];\n out += chars[((b1 & 3) << 4) | (b2 == undefined ? 0 : b2 >> 4)];\n out += b2 == undefined ? '=' : chars[((b2 & 15) << 2) | (b3 == undefined ? 0 : b3 >> 6)];\n out += b3 == undefined ? '=' : chars[b3 & 63];\n }\n return out;\n}\n\n// ============ 段评气泡(/content?review=keep 注入 <comment> 标记,点击打开 /review_html 页面) ============\nfunction createSvg(number, url) {\n var t = number > 99 ? '99+' : String(number);\n var svg = `<svg xmlns='http://www.w3.org/2000/svg' width='100' height='90' viewBox='0 0 25 17' fill='none'><path stroke='currentColor' stroke-width='1.00' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='4.00' style='stroke:#A7A7A7;stroke-opacity:0.5;stroke-width:1px;stroke-linejoin:round;fill:transparent;fill-opacity:0.5;' d='M 7.5 0.5 L 16.5 0.5 A 7 7 0 0 1 23.5 7.5 L 23.5 7.5 A 7 7 0 0 1 16.5 14.5 L 8.5 14.5 L 5.192 16.5 L 5.5 14.5 A 7 7 30 0 1 0.5 7.5 L 0.5 7.5 A 7 7 0 0 1 7.5 0Line truncated
"lastUpdateTime": 1791569184668,
"respondTime": 180000, "respondTime": 180000,
"ruleBookInfo": { "exploreUrl": "<js>\nvar groups = {\"🔥 男频精选\": [{\"title\": \"玄幻\", \"url\": \"/category-landing?category_id=7&offset={{(page-1)*20}}\"}, {\"title\": \"都市\", \"url\": \"/category-landing?category_id=1&offset={{(page-1)*20}}\"}, {\"title\": \"历史\", \"url\": \"/category-landing?category_id=12&offset={{(page-1)*20}}\"}, {\"title\": \"悬疑\", \"url\": \"/category-landing?category_id=10&offset={{(page-1)*20}}\"}, {\"title\": \"衍生\", \"url\": \"/category-landing?category_id=1079&offset={{(page-1)*20}}\"}, {\"title\": \"同人\", \"url\": \"/category-landing?category_id=538&offset={{(page-1)*20}}\"}, {\"title\": \"科幻末世\", \"url\": \"/category-landing?category_id=8&offset={{(page-1)*20}}\"}, {\"title\": \"奇幻仙侠\", \"url\": \"/category-landing?category_id=259&offset={{(page-1)*20}}\"}, {\"title\": \"种田\", \"url\": \"/category-landing?category_id=23&offset={{(page-1)*20}}\"}, {\"title\": \"游戏体育\", \"url\": \"/category-landing?category_id=746&offset={{(page-1)*20}}\"}, {\"title\": \"系统\", \"url\": \"/category-landing?category_id=19&offset={{(page-1)*20}}\"}, {\"title\": \"重生\", \"url\": \"/category-landing?category_id=36&offset={{(page-1)*20}}\"}, {\"title\": \"穿越\", \"url\": \"/category-landing?category_id=37&offset={{(page-1)*20}}\"}, {\"title\": \"多女主\", \"url\": \"/category-landing?category_id=91&offset={{(page-1)*20}}\"}, {\"title\": \"修仙\", \"url\": \"/category-landing?category_id=517&offset={{(page-1)*20}}\"}, {\"title\": \"二次元\", \"url\": \"/category-landing?category_id=39&offset={{(page-1)*20}}\"}, {\"title\": \"搞笑轻松\", \"url\": \"/category-landing?category_id=778&offset={{(page-1)*20}}\"}, {\"title\": \"单女主\", \"url\": \"/category-landing?category_id=389&offset={{(page-1)*20}}\"}, {\"title\": \"悬疑灵异\", \"url\": \"/category-landing?category_id=751&offset={{(page-1)*20}}\"}, {\"title\": \"末日求生\", \"url\": \"/category-landing?category_id=515&offset={{(page-1)*20}}\"}, {\"title\": \"娱乐圈\", \"url\": \"/category-landing?category_id=43&offset={{(page-1)*20}}\"}, {\"title\": \"末世\", \"url\": \"/category-landing?category_id=68&offset={{(page-1)*20}}\"}], \"🌸 女频精选\": [{\"title\": \"现代言情\", \"url\": \"/category-landing?category_id=3&offset={{(page-1)*20}}\"}, {\"title\": \"古代言情\", \"url\": \"/category-landing?category_id=5&offset={{(page-1)*20}}\"}, {\"title\": \"纯爱\", \"url\": \"/category-landing?category_id=1012&offset={{(page-1)*20}}\"}, {\"title\": \"甜宠\", \"url\": \"/category-landing?category_id=96&offset={{(page-1)*20}}\"}, {\"title\": \"总裁\", \"url\": \"/category-landing?category_id=29&offset={{(page-1)*20}}\"}, {\"title\": \"幻想言情\", \"url\": \"/category-landing?category_id=32&offset={{(page-1)*20}}\"}, {\"title\": \"双男主\", \"url\": \"/category-landing?category_id=275&offset={{(page-1)*20}}\"}, {\"title\": \"双女主\", \"url\": \"/category-landing?category_id=704&offset={{(page-1)*20}}\"}, {\"title\": \"年代\", \"url\": \"/category-landing?category_id=79&offset={{(page-1)*20}}\"}, {\"title\": \"快穿\", \"url\": \"/category-landing?category_id=24&offset={{(page-1)*20}}\"}, {\"title\": \"豪门总裁\", \"url\": \"/category-landing?category_id=748&offset={{(page-1)*20}}\"}, {\"title\": \"追妻火葬场\", \"url\": \"/category-landing?category_id=616&offset={{(page-1)*20}}\"}, {\"title\": \"无CP\", \"url\": \"/category-landing?category_id=392&offset={{(page-1)*20}}\"}, {\"title\": \"大女主\", \"url\": \"/category-landing?category_id=760&offset={{(page-1)*20}}\"}, {\"title\": \"女强\", \"url\": \"/category-landing?category_id=86&offset={{(page-1)*20}}\"}], \"🏷️ 男频标签\": [{\"title\": \"古代\", \"url\": \"/category-landing?category_id=758&offset={{(page-1)*20}}\"}, {\"title\": \"历史古代\", \"url\": \"/category-landing?category_id=273&offset={{(page-1)*20}}\"}, {\"title\": \"轻小说\", \"url\": \"/category-landing?category_id=858&offset={{(page-1)*20}}\"}, {\"title\": \"都市生活\", \"url\": \"/category-landing?category_id=2&offset={{(page-1)*20}}\"}, {\"title\": \"都市高武\", \"url\": \"/category-landing?category_id=1014&offset={{(page-1)*20}}\"}, {\"title\": \"游戏动漫\", \"url\": \"/category-landing?category_id=57&offset={{(page-1)*20}}\"}, {\"title\": \"抗战谍战\", \"url\": \"/category-landing?category_id=504&offset={{(page-1)*20}}\"}, {\"title\": \"体育\", \"url\": \"/category-landing?category_id=15&offset={{(page-1)*20}}\"}, {\"title\": \"都市异能\", \"url\": \"/category-landing?category_id=516&offset={{(page-1)*20}}\"}, {\"title\": \"武侠\", \"url\": \"/category-landing?category_id=16&offset={{(page-1)*20}}\"}, {\"title\": \"东方仙侠\", \"url\": \"/category-landing?category_id=1140&offset={{(page-1)*20}}\"}, {\"title\": \"东方玄幻\", \"url\": \"/category-landing?category_id=511&offset={{(page-1)*20}}\"}, {\"title\": \"男频衍生\", \"url\": \"/category-landing?categorLine truncated
"author": "$.data.author", "searchUrl": "<js>\nvar tab = getVariable('模式');\nvar m = String(key).match(/^([xtmdXTMD])[:::]/);\nvar q = String(key);\nif (m) {\n tab = {x: '小说', t: '听书', m: '漫画', d: '短剧'}[m[1].toLowerCase()];\n q = q.slice(2);\n}\njava.put('fq_search_tab', tab);\nvar typeMap = {'小说': 3, '听书': 2, '漫画': 8, '短剧': 11};\n`/search?query=${encodeURIComponent(q)}&tab_type=${typeMap[tab] || 3}&offset=${(page - 1) * 10}`;\n</js>",
"coverUrl": "$.data.thumb_url",
"init": "",
"intro": "$.data.abstract",
"kind": "$.data.category_v2[*].Name",
"lastChapter": "$.data.last_chapter_title",
"name": "$.data.book_name",
"tocUrl": "/catalog?book_id={{$.data.book_id}}",
"wordCount": "$.data.word_number"
},
"ruleContent": {
"content": "$.data.content"
},
"ruleExplore": {
"author": "author",
"bookList": "$.data.book_info[*]",
"bookUrl": "/detail?book_id={{$.book_id}}",
"coverUrl": "thumb_url",
"intro": "abstract",
"kind": "category",
"name": "book_name",
"wordCount": "word_number"
},
"ruleSearch": { "ruleSearch": {
"author": "author", "author": "author",
"bookList": "$.data[*].book_data[*]", "bookList": "<js>\nvar tab = java.get('fq_search_tab') || getVariable('模式');\nvar d = parseJsonSafely(result, {});\nvar list = [];\n(d.data || []).forEach(function (b) {\n if (b.book_data) {\n b.book_data.forEach(function (x) {\n x.fq_tab = tab;\n list.push(x);\n });\n } else if (b.video_data) {\n // 短剧字段与书籍不同,归一化为 book_data 结构(book_id 用 series_id 走通 detail/catalog)\n b.video_data.forEach(function (v) {\n list.push({\n book_id: v.series_id || v.vid,\n book_name: v.title || v.raw_book_name,\n author: (v.video_detail && v.video_detail.actor) || '',\n thumb_url: v.cover || '',\n abstract: v.sub_title || '',\n category: v.sub_title || '',\n fq_tab: tab\n });\n });\n }\n});\nJSON.stringify({data: list});\n</js>$.data",
"bookUrl": "/detail?book_id={{$.book_id}}", "bookUrl": "<js>\n'/detail?book_id=' + result.book_id + '&tab=' + (result.fq_tab || getVariable('模式'));\n</js>",
"checkKeyWord": "十日终焉", "checkKeyWord": "十日终焉",
"coverUrl": "thumb_url", "coverUrl": "thumb_url",
"intro": "abstract", "intro": "abstract",
@@ -47,13 +26,36 @@
"name": "book_name", "name": "book_name",
"wordCount": "word_number" "wordCount": "word_number"
}, },
"ruleExplore": {
"author": "author",
"bookList": "$.data.book_info[*]",
"bookUrl": "<js>\n'/detail?book_id=' + result.book_id + '&tab=' + (result.fq_tab || getVariable('模式'));\n</js>",
"coverUrl": "thumb_url",
"intro": "abstract",
"kind": "category",
"name": "book_name",
"wordCount": "word_number"
},
"ruleBookInfo": {
"author": "$.data.author",
"canReName": "1",
"coverUrl": "$.data.thumb_url",
"init": "<js>\njava.put('fq_tab', (String(baseUrl).match(/[?&]tab=([^&]+)/) || [])[1] || getVariable('模式'));\nresult;\n</js>",
"intro": "<js>\nvar tab = java.get('fq_tab') || getVariable('模式');\nvar x = (parseJsonSafely(result, {}) || {}).data || {};\nfunction v(t) { return (t != undefined && t != null && String(t).length > 0 && String(t) != 'null') ? String(t) : ''; }\nfunction cats() {\n if (x.category) return x.category;\n if (Array.isArray(x.category_v2)) return x.category_v2.map(function (c) { return c.Name || c.name || ''; }).filter(String).join('/');\n return '';\n}\nvar statusMap = {'0': '连载中', '1': '连载中', '2': '已完结', 'creation_status_end': '已完结', 'creation_status_loading': '连载中'};\nvar st = statusMap[v(x.creation_status)] || v(x.creation_status);\nvar lines = ['📡 来源:番茄 | 🔄 模式:' + tab];\n[['🌈 分类', cats()],\n ['📚 最新章节', v(x.last_chapter_title)],\n ['📝 字数', v(x.word_number)],\n ['🚩 状态', st],\n ['⭐ 评分', v(x.score)],\n ['👀 阅读人数', v(x.read_count)]\n].forEach(function (p) { if (p[1]) lines.push(' ' + p[0] + ':' + p[1]); });\nif (v(x.abstract)) lines.push('❇️─────────\\n📖 简介:\\n' + String(x.abstract).split('\\n').map(function (l) { return ' ' + l; }).join('\\n'));\nlines.join('\\n');\n</js>",
"kind": "$.data.category_v2[*].Name",
"lastChapter": "$.data.last_chapter_title",
"name": "$.data.book_name",
"tocUrl": "/catalog?book_id={{$.data.book_id}}",
"wordCount": "$.data.word_number"
},
"ruleToc": { "ruleToc": {
"chapterList": "$.data.item_data_list[*]", "chapterList": "<js>\njava.put('fq_book_id', (String(baseUrl).match(/book_id=(\\d+)/) || [])[1] || '');\n// 书籍类型按模式动态设置:安卓 8文本/32音频/64图片,iOS 0/1/2\nvar tab = java.get('fq_tab') || getVariable('模式');\nvar tMap = {'小说': 8, '听书': 32, '漫画': 64, '短剧': 8};\nvar t = tMap[tab] || 8;\nif (isIOS()) t = {'小说': 0, '听书': 1, '漫画': 2, '短剧': 0}[tab] || 0;\ntry { book.type = t; } catch (e) {}\nresult;\n</js>$.data.item_data_list[*]",
"chapterName": "title", "chapterName": "$.title",
"chapterUrl": "/content?item_id={{$.item_id}}&text_mode=plain&review_mode=remove" "chapterUrl": "<js>\nvar tab = java.get('fq_tab') || getVariable('模式');\nvar id = result.item_id;\nvar url;\nif (tab == '听书') {\n url = '/audio?item_ids=' + id + '&tone_id=' + getVariable('音色') + '&output=url';\n} else if (tab == '短剧') {\n url = '/video?item_ids=' + id + '&output=proxy&quality=' + getVariable('清晰度');\n} else {\n var review = getVariable('段评') != '关' ? 'keep' : 'remove';\n var img = getVariable('图片') == '移除' ? 'remove' : 'keep';\n url = '/content?item_id=' + id + '&text_mode=' + (review == 'keep' ? 'html' : 'plain') + '&review=' + review + '&image_mode=' + img;\n}\nurl;\n</js>"
},
"ruleContent": {
"content": "<js>\nvar base = BaseUrl();\nvar u = String(baseUrl);\nvar item = (u.match(/item_ids?=(\\d+)/) || [])[1] || '';\nvar tab = java.get('fq_tab') || getVariable('模式');\nfunction absUrl(s) { return (s && String(s).indexOf('http') != 0) ? base + s : s; }\nfunction videoUrl() {\n var v = parseJsonSafely(request('/video?item_ids=' + item + '&output=proxy&quality=' + getVariable('清晰度')), null);\n var vl = v && v.data && v.data.video_list;\n if (vl) { var ks = Object.keys(vl); if (ks.length) return absUrl(vl[ks[0]].main_url); }\n return '';\n}\nvar out = '';\nif (u.indexOf('/video') > -1) {\n // 短剧:解密代理直链,当前章节自动打开播放\n var d = parseJsonSafely(result, {});\n var vl = d.data && d.data.video_list;\n var vu = '';\n if (vl) { var ks = Object.keys(vl); if (ks.length) vu = absUrl(vl[ks[0]].main_url); }\n if (vu && book.durChapterIndex == chapter.index) {\n try { java.startBrowser(vu, title || ''); } catch (e) {}\n }\n out = vu ? `▶️ 正在加载视频…\\n若未自动播放,请复制以下直链到播放器打开:\\n${vu}` : '❌ 视频获取失败,请刷新重试';\n} else if (u.indexOf('/audio') > -1) {\n // 听书:短剧视频章优先原声音轨,其余回退 TTS 音色\n out = videoUrl();\n if (!out) {\n var a = parseJsonSafely(result, {});\n if (a.data && a.data[0] && a.data[0].main_url) out = absUrl(a.data[0].main_url);\n }\n} else {\n // 小说/漫画:段评气泡 + 空正文回退图片列表\n var review = getVariable('段评') != '关';\n var c = '';\n var d = parseJsonSafely(result, {});\n c = (d.data && d.data.content) || '';\n if (c && review) {\n // 段评 ident 域名由服务端按请求 Host 拼接,强制替换为书源地址\n c = c.replace(/(<comment ident=\")[^\"]*?\\/review_html\\?/g, '$1' + base + '/review_html?');\n c = isIOS() ? paraForiOS(c) : paraForAndroid(c);\n }\n if (c) {\n out = c;\n } else {\n var m = parseJsonSafely(request('/manga?item_ids=' + item + '&output=proxy'), {});\n // 兼容两种响应:data 为图片数组,或 data.images 为图片数组\n var imgs = (m && m.data && Array.isArray(m.data.images)) ? m.data.images : (m && Array.isArray(m.data) ? m.data : null);\n out = imgs ? imgs.map(function (i) { return `<img src=\"${absUrl(i.data)}\">`; }).join('\\n') : ((m && m.data && m.data.content) || '❌ 内容获取失败,请刷新重试');\n }\n}\nout;\n</js>"
}, },
"exploreUrl": "{\"🔥 男频精选\": [\"玄幻::/category-landing?category_id=7&offset={{(page-1)*20}}\", \"都市::/category-landing?category_id=1&offset={{(page-1)*20}}\", \"历史::/category-landing?category_id=12&offset={{(page-1)*20}}\", \"悬疑::/category-landing?category_id=10&offset={{(page-1)*20}}\", \"衍生::/category-landing?category_id=1079&offset={{(page-1)*20}}\", \"同人::/category-landing?category_id=538&offset={{(page-1)*20}}\", \"科幻末世::/category-landing?category_id=8&offset={{(page-1)*20}}\", \"奇幻仙侠::/category-landing?category_id=259&offset={{(page-1)*20}}\", \"种田::/category-landing?category_id=23&offset={{(page-1)*20}}\", \"游戏体育::/category-landing?category_id=746&offset={{(page-1)*20}}\", \"系统::/category-landing?category_id=19&offset={{(page-1)*20}}\", \"重生::/category-landing?category_id=36&offset={{(page-1)*20}}\", \"穿越::/category-landing?category_id=37&offset={{(page-1)*20}}\", \"多女主::/category-landing?category_id=91&offset={{(page-1)*20}}\", \"修仙::/category-landing?category_id=517&offset={{(page-1)*20}}\", \"二次元::/category-landing?category_id=39&offset={{(page-1)*20}}\", \"搞笑轻松::/category-landing?category_id=778&offset={{(page-1)*20}}\", \"单女主::/category-landing?category_id=389&offset={{(page-1)*20}}\", \"悬疑灵异::/category-landing?category_id=751&offset={{(page-1)*20}}\", \"末日求生::/category-landing?category_id=515&offset={{(page-1)*20}}\", \"娱乐圈::/category-landing?category_id=43&offset={{(page-1)*20}}\", \"末世::/category-landing?category_id=68&offset={{(page-1)*20}}\"], \"🌸 女频精选\": [\"现代言情::/category-landing?category_id=3&offset={{(page-1)*20}}\", \"古代言情::/category-landing?category_id=5&offset={{(page-1)*20}}\", \"纯爱::/category-landing?category_id=1012&offset={{(page-1)*20}}\", \"甜宠::/category-landing?category_id=96&offset={{(page-1)*20}}\", \"总裁::/category-landing?category_id=29&offset={{(page-1)*20}}\", \"幻想言情::/category-landing?category_id=32&offset={{(page-1)*20}}\", \"双男主::/category-landing?category_id=275&offset={{(page-1)*20}}\", \"双女主::/category-landing?category_id=704&offset={{(page-1)*20}}\", \"年代::/category-landing?category_id=79&offset={{(page-1)*20}}\", \"快穿::/category-landing?category_id=24&offset={{(page-1)*20}}\", \"豪门总裁::/category-landing?category_id=748&offset={{(page-1)*20}}\", \"追妻火葬场::/category-landing?category_id=616&offset={{(page-1)*20}}\", \"无CP::/category-landing?category_id=392&offset={{(page-1)*20}}\", \"大女主::/category-landing?category_id=760&offset={{(page-1)*20}}\", \"女强::/category-landing?category_id=86&offset={{(page-1)*20}}\"], \"🏷️ 男频标签\": [\"古代::/category-landing?category_id=758&offset={{(page-1)*20}}\", \"历史古代::/category-landing?category_id=273&offset={{(page-1)*20}}\", \"轻小说::/category-landing?category_id=858&offset={{(page-1)*20}}\", \"都市生活::/category-landing?category_id=2&offset={{(page-1)*20}}\", \"都市高武::/category-landing?category_id=1014&offset={{(page-1)*20}}\", \"游戏动漫::/category-landing?category_id=57&offset={{(page-1)*20}}\", \"抗战谍战::/category-landing?category_id=504&offset={{(page-1)*20}}\", \"体育::/category-landing?category_id=15&offset={{(page-1)*20}}\", \"都市异能::/category-landing?category_id=516&offset={{(page-1)*20}}\", \"武侠::/category-landing?category_id=16&offset={{(page-1)*20}}\", \"东方仙侠::/category-landing?category_id=1140&offset={{(page-1)*20}}\", \"东方玄幻::/category-landing?category_id=511&offset={{(page-1)*20}}\", \"男频衍生::/category-landing?category_id=1016&offset={{(page-1)*20}}\", \"西方奇幻::/category-landing?category_id=1141&offset={{(page-1)*20}}\", \"异世大陆::/category-landing?category_id=512&offset={{(page-1)*20}}\", \"都市种田::/category-landing?category_id=263&offset={{(page-1)*20}}\", \"影视小说::/category-landing?category_id=45&offset={{(page-1)*20}}\", \"仕途::/category-landing?category_id=558&offset={{(page-1)*20}}\", \"规则怪谈::/category-landing?category_id=851&offset={{(page-1)*20}}\", \"国运::/category-landing?category_id=496&offset={{(page-1)*20}}\", \"灵气复苏::/category-landing?category_id=514&offset={{(page-1)*20}}\", \"历史脑洞::/category-landing?category_id=272&offset={{(page-1)*20}}\", \"都市脑洞::/category-landing?category_id=262&offset={{(page-1)*20}}\", \"玄幻脑洞::/category-landing?category_id=257&offset={{(page-1)*20}}\", \"综漫::/category-landing?category_id=465&offset={{(page-1)*20}}\", \"综影视::/category-landing?category_id=1058&offset={{(page-1)*20}}\", \"架空::/category-landing?category_id=452&offset={{(page-1)*20}}\", \"异能::/category-landing?category_id=598&offset={{(page-1)*20}}\", \"克苏鲁::/category-landing?category_id=705&offset={{(page-1)*20}}\", \"高武世界::/category-landing?category_id=513&offset={{(page-1)*20}}\", \"悬疑脑洞::/categoryLine truncated
"searchUrl": "/search?query={{key}}&tab_type=3&offset={{(page-1)*10}}",
"weight": 0 "weight": 0
} }
] ]
+63 -9
View File
@@ -212,14 +212,12 @@ restart_and_verify() {
# 写入 nginx 配置(--init 与 --sync-nginx 共用的唯一模板来源) # 写入 nginx 配置(--init 与 --sync-nginx 共用的唯一模板来源)
# 模板自带完整 443/证书配置(证书存在时启用),不依赖 certbot --nginx 注入——
# 这样 --sync-nginx 覆盖配置时不会丢失 HTTPS 段(certbot 注入式配置会被覆盖回 80-only)
write_nginx_conf() { write_nginx_conf() {
$SUDO tee "${NGINX_CONF}" >/dev/null <<NGINX_EOF # 反代 location(80 无证书时与 443 共用)
server { local proxy_locations
listen 80; proxy_locations=$(cat <<LOC_EOF
server_name ${DOMAIN};
access_log /var/log/nginx/${APP_NAME}.access.log;
error_log /var/log/nginx/${APP_NAME}.error.log;
# 视频解密代理:关闭缓冲,分片(Range/206)直通,避免 nginx 对大文件二次落盘 # 视频解密代理:关闭缓冲,分片(Range/206)直通,避免 nginx 对大文件二次落盘
location /video/proxy/ { location /video/proxy/ {
@@ -246,9 +244,60 @@ server {
proxy_read_timeout 300s; proxy_read_timeout 300s;
proxy_send_timeout 300s; proxy_send_timeout 300s;
} }
LOC_EOF
)
local ssl_block=""
if [ -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
log "检测到 ${DOMAIN} 证书,配置启用 443"
ssl_block=$(cat <<SSL_EOF
server {
listen 443 ssl http2;
server_name ${DOMAIN};
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/${APP_NAME}.access.log;
error_log /var/log/nginx/${APP_NAME}.error.log;
${proxy_locations}
} }
SSL_EOF
)
else
warn "未找到 /etc/letsencrypt/live/${DOMAIN}/ 证书,本次仅写 80 配置(不跳转 HTTPS);证书签发后重跑: $0 --sync-nginx"
fi
# 80 端口:有证书时仅 ACME 验证 + 301 跳转;无证书时直接反代
local root_location
if [ -n "${ssl_block}" ]; then
root_location=' location / {
return 301 https://$host$request_uri;
}'
else
root_location="${proxy_locations}"
fi
$SUDO tee "${NGINX_CONF}" >/dev/null <<NGINX_EOF
server {
listen 80;
server_name ${DOMAIN};
access_log /var/log/nginx/${APP_NAME}.access.log;
error_log /var/log/nginx/${APP_NAME}.error.log;
# ACME http-01 验证(certbot webroot 模式依赖)
location /.well-known/acme-challenge/ {
root /var/www/html;
}
${root_location}
}${ssl_block}
NGINX_EOF NGINX_EOF
} }
# 更新已部署服务器的 nginx 配置:备份 → 重写 → 校验 → 失败回滚 # 更新已部署服务器的 nginx 配置:备份 → 重写 → 校验 → 失败回滚
sync_nginx_conf() { sync_nginx_conf() {
local backup="" local backup=""
@@ -428,8 +477,13 @@ HOOK
if [ ! -d "/etc/letsencrypt/live/${DOMAIN}" ]; then if [ ! -d "/etc/letsencrypt/live/${DOMAIN}" ]; then
if command -v certbot >/dev/null 2>&1; then if command -v certbot >/dev/null 2>&1; then
log "未检测到证书,尝试自动申请: ${DOMAIN}" log "未检测到证书,尝试自动申请: ${DOMAIN}"
$SUDO certbot --nginx -d "${DOMAIN}" --non-interactive --agree-tos -m "${CERT_EMAIL}" --redirect \ $SUDO mkdir -p /var/www/html
|| warn "证书申请失败,请检查域名解析与 80 端口;可稍后手动执行 certbot --nginx -d ${DOMAIN}" if $SUDO certbot certonly --webroot -w /var/www/html -d "${DOMAIN}" --non-interactive --agree-tos -m "${CERT_EMAIL}"; then
log "证书申请成功,写入 443 配置..."
write_nginx_conf
else
warn "证书申请失败,请检查域名解析与 80 端口;成功后重跑: $0 --sync-nginx"
fi
else else
warn "未安装 certbot,跳过证书申请。可手动执行: dnf install certbot python3-certbot-nginx" warn "未安装 certbot,跳过证书申请。可手动执行: dnf install certbot python3-certbot-nginx"
fi fi