From 0334c891267d797e1528b2188ffac820c7a047e1 Mon Sep 17 00:00:00 2001 From: siritami <102145692+FiorenMas@users.noreply.github.com> Date: Sat, 23 May 2026 10:33:30 +0700 Subject: [PATCH 1/4] STV: Smooth cf captcha, change structure code --- .../SangTacViet/content-normalization.ts | 97 +++ .../vietnamese/SangTacViet/external-url.ts | 168 +++++ plugins/vietnamese/SangTacViet/filters.ts | 127 ++++ .../vietnamese/SangTacViet/glyph-decode.ts | 56 ++ plugins/vietnamese/SangTacViet/index.ts | 611 +++--------------- .../vietnamese/SangTacViet/webview/index.js | 360 +++++------ 6 files changed, 731 insertions(+), 688 deletions(-) create mode 100644 plugins/vietnamese/SangTacViet/content-normalization.ts create mode 100644 plugins/vietnamese/SangTacViet/external-url.ts create mode 100644 plugins/vietnamese/SangTacViet/filters.ts create mode 100644 plugins/vietnamese/SangTacViet/glyph-decode.ts diff --git a/plugins/vietnamese/SangTacViet/content-normalization.ts b/plugins/vietnamese/SangTacViet/content-normalization.ts new file mode 100644 index 00000000..34c249d7 --- /dev/null +++ b/plugins/vietnamese/SangTacViet/content-normalization.ts @@ -0,0 +1,97 @@ +import { decodeHtmlEntities, encodeHtmlEntities } from '@libs/utils'; +import { decodeGlyphs } from './glyph-decode'; + +// ── Content normalization ───────────────── +function looksLikeInterlinear(text: string): boolean { + return /]*\b(?:t|v|p)\s*=\s*['"][^'"]*['"][^>]*>/i.test(text); +} + +function normalizeInterlinear(raw: string): string { + let t = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + t = t.replace(/\s*/gi, '\n').replace(/<\/br\s*>/gi, '\n'); + t = t.replace( + /<\/i>\s*\n+\s*(?=[,.;:!?%\)\]\}\u3002\uff0c\u3001\uff01\uff1f\uff1b\uff1a\u201d\u2019\u300d\u300f\u3011\u300b])/gi, + '', + ); + const marker = '__STV_GAP__'; + t = t.replace(/<\/i>\s*' + marker + ']*>([\s\S]*?)<\/i>/gi, '$1'); + t = t.replace( + /<\/?(p|div|article|section|li|tr|h[1-6]|blockquote|ul|ol)[^>]*>/gi, + '\n', + ); + t = t.replace(/<[^>]+>/g, '').replace(/[<>]/g, ''); + t = decodeHtmlEntities(t); + t = t.replace(new RegExp(marker, 'g'), ' '); + t = t.replace(/[\t\f\v ]+/g, ' '); + t = t.replace( + / +([,.;:!?%\)\]\}\u3002\uff0c\u3001\uff01\uff1f\uff1b\uff1a\u201d\u2019\u300d\u300f\u3011\u300b])/g, + '$1', + ); + t = t.replace( + /\n+([,.;:!?%\)\]\}\u3002\uff0c\u3001\uff01\uff1f\uff1b\uff1a\u201d\u2019\u300d\u300f\u3011\u300b])/g, + '$1', + ); + t = t + .replace(/[ ]*\n[ ]*/g, '\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); + return t ? t.replace(/\n/g, '
') : ''; +} + +export function normalizeChapterHtml(host: string, raw: string): string { + let text = raw || ''; + if (!text) return ''; + const h = host.toLowerCase(); + + if (h === 'fanqie') { + text = text.replace(/]*>[\s\S]*?<\/header>/gi, ''); + text = text.replace(/]*>[\s\S]*?<\/footer>/gi, ''); + text = text.replace(/<\/?article>/gi, ''); + text = text.replace(/\sidx="\d+"/g, ''); + if (looksLikeInterlinear(text)) return normalizeInterlinear(text); + return text; + } + + if (h === 'sangtac' || h === 'dich') { + text = decodeGlyphs(text); + if (looksLikeInterlinear(text)) return normalizeInterlinear(text); + if (!/<\w+[^>]*>/.test(text)) { + text = text + .replace(/&/g, '&') + .replace(//g, '>'); + text = text + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/\n/g, '
'); + } + return text; + } + + if (!/<\w+[^>]*>/.test(text)) { + text = text + .replace(/&/g, '&') + .replace(//g, '>'); + text = text + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/\n/g, '
'); + return text; + } + if (looksLikeInterlinear(text)) return normalizeInterlinear(text); + return text; +} + +export function wrapWithParagraphs(rawText: string): string { + if (!rawText) return ''; + const cleanText = rawText.replace(//gi, '\n'); + const paragraphs = cleanText.split('\n'); + const htmlResult = paragraphs + .map(line => line.trim()) + .filter(line => line.length > 0) + .map(line => `

${encodeHtmlEntities(line)}

`) + .join('\n'); + return htmlResult; +} diff --git a/plugins/vietnamese/SangTacViet/external-url.ts b/plugins/vietnamese/SangTacViet/external-url.ts new file mode 100644 index 00000000..e68f20e8 --- /dev/null +++ b/plugins/vietnamese/SangTacViet/external-url.ts @@ -0,0 +1,168 @@ +/* eslint-disable no-useless-escape */ + +// ── External-URL +const HOST_PATTERNS: Record = { + uukanshu: [ + 'uukanshu\\.(?:com|net)/b/(\\d+)/(\\d+)?(.html)?', + 'sj\\.uukanshu\\.(?:com|net)//?book(?:_amp)?\\.aspx\\?id=(\\d+)', + 'sj\\.uukanshu\\.(?:com|net)//?read\\.aspx?tid=(\\d+)&sid=(\\d+)', + 'zhaoshuyuan\\.(?:com|net)/b/(\\d+)/(\\d+)?(.html)?', + 'zhaoshuyuan\\.(?:com|net)//?book(?:_amp)?\\.aspx\\?id=(\\d+)', + 'zhaoshuyuan\\.(?:com|net)//?read\\.aspx?tid=(\\d+)&sid=(\\d+)', + ], + '69shu': [ + '69shuba\\.com/(?:txt|book/)?(\\d+)/?(\\d+)?', + '69shuba\\.cx/(?:txt|book/)?(\\d+)/?(\\d+)?', + '69xinshu\\.com/(?:txt|book/)?(\\d+)/?(\\d+)?', + '69shu\\.pro/(?:txt|book/)?(\\d+)/?(\\d+)?', + '69shu\\.[a-z]{3,4}/(?:txt|book/)?(\\d+)/?(\\d+)?', + '69shuba\\.[a-z]{3,4}/(?:txt|book/)?(\\d+)/?(\\d+)?', + ], + '69shuorg': ['69shu\\.org/book[_/](\\d+)/(\\d+)?'], + xiaoqiangwx: ['xiaoqiangwx\\.org/(?:\\d+|book)/(\\d+)/?(\\d+)?(.html)?'], + cuiweijux: ['cuiweijux\\.com/files/article/html/\\d+/(\\d+)/(\\d+)?(.html)?'], + biquge: [ + 'biquge\\.com\\.cn/book/(\\d+)/(\\d+)?(.html)?', + 'sobiquge\\.com/book/(\\d+)/', + '81zw\\.org/books/(\\d+)/', + ], + trxs: ['trxs\\.cc/tongren/(\\d+)/?(\\d+)?(.html)?'], + ikshu8: ['ikshu8\\.com/book/(\\d+)/?(\\d+)?(.html)?'], + shulinw: [ + 'shulinw\\.com/(?:shu/|yuedu/|book/|\\d+/|modules/article/articleinfo\\.php\\?id=)(\\d+)(?:/)?(\\d*)(.html)?', + ], + wuxia1: [ + 'wuxia1\\.com/(?:shu/|yuedu/|book/|\\d+/|modules/article/articleinfo\\.php\\?id=)(\\d+)(?:/)?(\\d*)(.html)?', + ], + shu05: ['shu05\\.com/\\d+[/_](\\d+)/(\\d*)(.html)?'], + kuhu168: ['kuhu168\\.com/\\d+[/_](\\d+)/(\\d*)(.html)?'], + '2kxs': ['2kxs\\.org/\\d+[/_](\\d+)/(\\d*)(.html)?'], + yikanxiaoshuo: ['yikanxiaoshuo\\.com/\\d+[/_](\\d+)/(\\d*)(.html)?'], + '8zwdu': ['8zwdu\\.com/\\d+[/_](\\d+)/(\\d*)(.html)?'], + kanmaoxian: ['kanmaoxian\\.com/(?:book|\\d+)/(\\d+)/?(\\d*)(.html)?'], + kayegenet: ['kayege\\.net/(?:book|\\d+)[/_](\\d+)/?(\\d*)(.html)?'], + '4gxsw': ['4gxsw\\.com/(?:book|html/\\d+)/(\\d+)/?(\\d*)(.html)?'], + qinqinxsw: ['qinqinxsw\\.com/(?:book|\\d+)[/_](\\d+)/?(\\d*)(.html)?'], + read8: ['read8\\.net/(?:dushu)/(\\d+)/(\\d*)(.html)?'], + ciweimao: ['ciweimao\\.com/book/(\\d+)'], + wkkshu: ['wkkshu\\.com/(?:xs/\\d+/|\\d+_)(\\d+)/(\\d*)(.html)?'], + '168kanshu': ['168kanshu\\.com/(?:xs/\\d+/|\\d+_)(\\d+)/(\\d*)(.html)?'], + wanbentxt: ['wanbentxt\\.com/(\\d+)/([\\d_]*)(.html)?'], + '38kanshu': ['mijiashe\\.com/(\\d+)/([\\d_]*)(.html)?'], + duanqingsi: ['duanqingsi\\.com/(\\d+)/([\\d_]*)(.html)?'], + faloo: [ + '\\.faloo\\.com/[pfboklithtm]+/(\\d+)(?:\\.html|/(\\d+)\\.html)?', + '\\.faloo\\.com/(\\d+)\\.html', + '\\.faloo\\.com/(\\d+)_(\\d+)\\.html', + ], + qiuxiaoshuo: ['qiuxiaoshuo\\.com/(?:book|read)[/-](\\d+)[/-]?(\\d*)'], + dibaqu123: ['dibaqu123\\.com/\\d+/(\\d+)/?(\\d*)(.html)?'], + jiacuan: ['jiacuan\\.com/\\d+/(\\d+)/(\\d*)(.html)?'], + shubaow: ['shubaow\\.net/\\d+[_/](\\d+)/(\\d*)(.html)?'], + biqugeinfo: ['biquge\\.info/\\d+_(\\d+)/(\\d*)(.html)?'], + shumilou: ['shumilou\\.net/\\d+/(\\d+)/(\\d*)(.html)?'], + xbiquge: ['xbiquge\\.cc/book/(\\d+)/(\\d*)(.html)?'], + paoshu8: ['paoshu8\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], + duokan8: ['duokan8\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], + biqugecom: ['biquge\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], + hetushu: ['hetushu\\.com/book/(\\d+)/(\\d*)(.html)?'], + nofff: ['nofff\\.com/(\\d+)/(\\d*)/?'], + uuxs: ['uuxs\\.tw/ls/\\d+_(\\d+)/(\\d*)(.html)?'], + ranwenla: ['ranwen\\.la/files/article/\\d+/(\\d+)/(\\d*)(.html)?'], + '66wx': ['66wx\\.com/(\\d+)_\\d+/read(\\d*)(.html)?'], + biqugexs: ['biqugexs\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], + '230book': ['230book\\.[comnet]{3}/book/(\\d+)/(\\d*)(.html)?'], + biqubu: ['biqubu\\.com/book_(\\d+)/(\\d*)(.html)?'], + '521danmei': [ + '521danmei\\.org/read/(\\d+)/(\\d*)/?', + '521danmei\\.org/book/(\\d+)\\.html', + ], + bxwxorg: [ + 'bxwxorg\\.com/read/(\\d+)/(\\d*)(.html)?', + 'bxwxorg\\.com/book/(\\d+)\\.html', + ], + qidian: ['qidian\\.com/(?:book|info)/(\\d+)'], + zwdu: ['zwdu\\.com/book/(\\d+)/(\\d*)(.html)?'], + zongheng: [ + 'book\\.zongheng\\.com/chapter/(\\d+)/(\\d*)(.html)?', + 'book\\.zongheng\\.com/book/(\\d+)(?:\\.html)?', + ], + biqugese: [ + 'biquge\\.se/(\\d+)/(\\d*)(.html)?', + 'biqugse\\.com/(\\d+)/(\\d*)(.html)?', + ], + qiushubang: ['qiushubang\\.com/(\\d+)/(\\d*)(.html)?'], + xinshuhaige: ['xinshuhaige\\.com/(\\d+)/(\\d*)(.html)?'], + oldtimescc: ['oldtimescc\\.cc/go/(\\d+)/(\\d*)(.html)?'], + wuwuxs: ['wuwuxs\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], + hs313: ['hs313\\.net/book/(\\d+)/(\\d*)(.html)?'], + shuchong: ['shuchong\\.info/chapter/(\\d+)/(\\d*)(.html)?'], + shucw: ['shucw\\.com/html/\\d+/(\\d+)/(\\d*)(.html)?'], + shumizu: ['shumizu\\.com/\\d+/(\\d+)/(\\d*)(.html)?'], + tadu: ['tadu\\.com/book/(\\d+)/?(\\d*)/?'], + ptwxz: [ + 'ptwxz\\.com/bookinfo/\\d+/(\\d+)\\.html', + 'ptwxz\\.com/html/\\d+/(\\d+)/(\\d+)\\.html', + 'ptwxz\\.com/html/\\d+/(\\d+)/', + ], + x81zw: ['x81zw\\.com/book/\\d+/(\\d+)/(\\d*)(.html)?'], + linovel: ['linovel\\.net/book/(\\d+)\\.html'], + wenku8: ['wenku8\\.net/novel/\\d+/(\\d+)/(\\d*)\\.htm'], + youyoukanshu: [ + 'youyoukanshu\\.com/book/(\\d+)\\.html', + 'youyoukanshu\\.com/book/(\\d+)/(\\d*)(.html)?', + ], + biqubao: ['biqubao\\.com/book/(\\d+)/(\\d*)(.html)?'], + biqugele: ['biqugele\\.com/txt/(\\d+)/(\\d*)(.html)?'], + biqugebz: ['biquge\\.bz/(\\d+)/(\\d*)(.html)?'], + biquge5200: ['biquge5200\\.cc/\\d+_(\\d+)/(\\d*)(.html)?'], + sfacg: [ + 'sfacg\\.com/(?:Novel|b|i)/(\\d+)/?', + 'sfacg\\.com/Novel/(\\d+)/MainIndex/', + ], + shubao45: ['shubao45\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], + kujiang: ['kujiang\\.com/book/(\\d+)'], + yushubo: [ + 'yushubo\\.com/book_(\\d+)', + 'yushubo\\.net/book_(\\d+)', + 'yushugu\\.com/book_(\\d+)', + 'yushugu\\.com/list_other_(\\d+)', + 'yushugu\\.com/read_(\\d+)_(\\d+)\\.html', + ], + xklxsw: ['xklxsw\\.com/book/(\\d+)/'], + fanqie: ['fanqienovel\\.com/page/(\\d+)'], + xsbiquge: ['xsbiquge\\.net/\\d+_(\\d+)/(\\d*)(.html)?'], + jjwxc: [ + 'jjwxc\\.net//?onebook\\.php\\?novelid=(\\d+)', + 'jjwxc\\.net//?book2/(\\d+)/?', + ], + qimao: ['qimao\\.com/shuku/(\\d+)/'], + ddxs: ['ddxs\\.com/([a-z0-9A-Z\\-_]+)/(\\d+)?'], + quanben5: ['quanben5\\.com/n/([a-z0-9A-Z\\-_]+)/'], + idejian: ['idejian\\.com/book/([0-9]+)/'], +}; + +// Hosts whose first capture is alphanumeric and must be resolved to a numeric +// bookid via /index.php?sajax=tryaddabtrecord (mirrors `isAbtHost()`). +export const ABT_HOSTS = new Set(['ddxs', 'quanben5', 'colamanga']); + +export function looksLikeExternalUrl(s: string): boolean { + const t = s.toLowerCase(); + return t.startsWith('http://') || t.startsWith('https://'); +} + +export function detectHostFromUrl( + input: string, +): { host: string; bookid: string; chapterid?: string } | null { + for (const host in HOST_PATTERNS) { + for (const pat of HOST_PATTERNS[host]) { + const m = new RegExp(pat, 'i').exec(input); + if (!m) continue; + if (m[2] !== undefined && m[2] !== m[1] && /^[\d_\-]+$/.test(m[2])) { + if (m[1]) return { host, bookid: m[1], chapterid: m[2] }; + } + if (m[1]) return { host, bookid: m[1] }; + } + } + return null; +} diff --git a/plugins/vietnamese/SangTacViet/filters.ts b/plugins/vietnamese/SangTacViet/filters.ts new file mode 100644 index 00000000..620d7c59 --- /dev/null +++ b/plugins/vietnamese/SangTacViet/filters.ts @@ -0,0 +1,127 @@ +import { type Filters, FilterTypes } from '@libs/filterInputs'; + +export const filters = { + sort: { + type: FilterTypes.Picker, + label: 'Sắp xếp', + value: 'view', + options: [ + { label: 'Không sắp xếp', value: '' }, + { label: 'Mới nhập kho', value: 'new' }, + { label: 'Mới cập nhật', value: 'update' }, + { label: 'Lượt đọc tổng', value: 'view' }, + { label: 'Lượt đọc tuần', value: 'viewweek' }, + { label: 'Lượt đọc ngày', value: 'viewday' }, + { label: 'Lượt thích', value: 'like' }, + { label: 'Lượt theo dõi', value: 'following' }, + { label: 'Lượt đánh dấu', value: 'bookmarked' }, + { label: 'Đề cử', value: 'auto' }, + ], + }, + minc: { + type: FilterTypes.Picker, + label: 'Số chương tối thiểu', + value: '0', + options: [ + { label: 'Tất cả', value: '0' }, + { label: '> 50', value: '50' }, + { label: '> 100', value: '100' }, + { label: '> 200', value: '200' }, + { label: '> 500', value: '500' }, + { label: '> 1000', value: '1000' }, + { label: '> 1500', value: '1500' }, + { label: '> 2000', value: '2000' }, + ], + }, + category: { + type: FilterTypes.Picker, + label: 'Thể loại chính', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Huyền huyễn', value: 'hh' }, + { label: 'Đô thị', value: 'dt' }, + { label: 'Ngôn tình', value: 'nt' }, + { label: 'Võng du', value: 'vd' }, + { label: 'Khoa học viễn tưởng', value: 'kh' }, + { label: 'Lịch sử', value: 'ls' }, + { label: 'Đồng nhân', value: 'dn' }, + { label: 'Dị năng', value: 'dna' }, + { label: 'Linh dị', value: 'ld' }, + { label: 'Light Novel', value: 'ln' }, + ], + }, + type: { + type: FilterTypes.Picker, + label: 'Loại truyện', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Truyện sáng tác', value: 'sangtac' }, + { label: 'Truyện dịch', value: 'dich' }, + { label: 'Truyện tranh', value: 'comic' }, + { label: 'Txt dịch tự động', value: 'txt' }, + { label: 'Truyện scan ảnh', value: 'scan' }, + ], + }, + step: { + type: FilterTypes.Picker, + label: 'Trạng thái đăng', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Hoàn thành', value: '3' }, + { label: 'Còn tiếp', value: '1' }, + { label: 'Tạm ngưng', value: '2' }, + { label: 'Không tạm ngưng', value: '5' }, + ], + }, + host: { + type: FilterTypes.Picker, + label: 'Nguồn truyện', + value: '', + options: [ + { label: 'Tất cả', value: '' }, + { label: '[Vip] fanqie (Cà Chua)', value: 'fanqie' }, + { label: '[Vip] qidian (Khởi Điểm)', value: 'qidian' }, + { label: '[Vip] ciweimao', value: 'ciweimao' }, + { label: '[Vip] faloo (Phi Lư)', value: 'faloo' }, + { label: '[Vip] jjwxc (Tấn Giang)', value: 'jjwxc' }, + { label: '[Vip] sfacg', value: 'sfacg' }, + { label: '[Vip] zongheng (Tung Hoành)', value: 'zongheng' }, + { label: '[Free] 69shu', value: '69shu' }, + { label: '[Free] tadu', value: 'tadu' }, + { label: '[Free] qimao (7 Mèo)', value: 'qimao' }, + { label: '[Free] idejian', value: 'idejian' }, + { label: '[LN] linovel', value: 'linovel' }, + { label: '[LN] wenku8', value: 'wenku8' }, + ], + }, + tag: { + type: FilterTypes.CheckboxGroup, + label: 'Nhãn', + value: [], + options: [ + { label: 'Đô Thị', value: 'dothi' }, + { label: 'Xuyên Qua', value: 'xuyenqua' }, + { label: 'Hệ Thống', value: 'hethong' }, + { label: 'Huyền Huyễn', value: 'huyenhuyen' }, + { label: 'Ngôn Tình', value: 'ngontinh' }, + { label: 'Đồng Nhân', value: 'dongnhan' }, + { label: 'Trùng Sinh', value: 'trungsinh' }, + { label: 'Lịch Sử', value: 'lichsu' }, + { label: 'Khoa Huyễn', value: 'khoahuyen' }, + { label: 'Tiên Hiệp', value: 'tienhiep' }, + { label: 'Võ Hiệp', value: 'vohiep' }, + { label: 'Sảng Văn', value: 'sangvan' }, + { label: 'Light Novel', value: 'lightnovel' }, + { label: 'Linh Dị', value: 'linhdi' }, + { label: 'Kỳ Huyễn', value: 'kyhuyen' }, + { label: 'Tận Thế', value: 'tanthe' }, + { label: 'Ngọt Sủng', value: 'ngotsung' }, + { label: 'Sân Trường', value: 'santruong' }, + { label: 'Nhiệt Huyết', value: 'nhiethuyet' }, + { label: 'Nhanh Xuyên', value: 'nhanhxuyen' }, + ], + }, +} satisfies Filters; diff --git a/plugins/vietnamese/SangTacViet/glyph-decode.ts b/plugins/vietnamese/SangTacViet/glyph-decode.ts new file mode 100644 index 00000000..92127ae6 --- /dev/null +++ b/plugins/vietnamese/SangTacViet/glyph-decode.ts @@ -0,0 +1,56 @@ +// ── Webfont glyph decode ───────────────────────────── +const GLYPH_MAP: Record = {}; +// prettier-ignore +([ + [0xE01B, 'A'], [0xE01E, 'y'], [0xE05F, '3'], [0xE063, 'z'], [0xE06B, 'K'], [0xE06C, 't'], + [0xE089, 'l'], [0xE0D5, 'S'], [0xE0D6, 'T'], [0xE100, 'o'], [0xE101, 'P'], [0xE116, '4'], + [0xE122, 'W'], [0xE124, 'Z'], [0xE14B, 'J'], [0xE160, 'e'], [0xE184, 'O'], [0xE186, 'D'], + [0xE1A4, 'f'], [0xE1AD, 'e'], [0xE1B4, 'k'], [0xE1B8, 'f'], [0xE1BF, 'n'], [0xE1C0, 'Y'], + [0xE1C1, '1'], [0xE1D8, 'K'], [0xE1E4, 'M'], [0xE1EA, 'Y'], [0xE215, 'C'], [0xE218, 'A'], + [0xE22B, 'h'], [0xE240, 'x'], [0xE248, 'v'], [0xE257, 'G'], [0xE27E, 'b'], [0xE2A9, 'B'], + [0xE2C5, 's'], [0xE2C7, 't'], [0xE2CA, 'G'], [0xE2E3, 'k'], [0xE2F8, 'q'], [0xE30F, 'F'], + [0xE311, 'u'], [0xE32F, 'E'], [0xE334, '2'], [0xE34A, 'I'], [0xE37C, 'R'], [0xE38F, 'v'], + [0xE39B, 'X'], [0xE3B0, 'l'], [0xE3B7, '7'], [0xE3F1, 'l'], [0xE41B, 'o'], [0xE41C, 'H'], + [0xE426, 'S'], [0xE427, 'J'], [0xE43E, '6'], [0xE44E, 'X'], [0xE46A, 'b'], [0xE477, 'y'], + [0xE49A, 'c'], [0xE4A3, '8'], [0xE4AE, '2'], [0xE4CC, 's'], [0xE4D3, '5'], [0xE4DB, 'L'], + [0xE4DF, 'N'], [0xE4EC, '5'], [0xE4F3, 'r'], [0xE519, '0'], [0xE51F, 'g'], [0xE550, 'E'], + [0xE557, 'h'], [0xE566, 'N'], [0xE571, 'F'], [0xE57B, 'O'], [0xE5BD, 'C'], [0xE5C1, 'd'], + [0xE5C9, '8'], [0xE5D1, 'x'], [0xE5DC, 'm'], [0xE5E1, '9'], [0xE5F0, 'u'], [0xE5FA, 'm'], + [0xE5FF, 'a'], [0xE603, 'U'], [0xE62A, 'w'], [0xE636, 'P'], [0xE63E, 'D'], [0xE648, '6'], + [0xE65B, 'H'], [0xE65D, 'z'], [0xE660, '9'], [0xE68D, '1'], [0xE691, 'M'], [0xE6A4, 'q'], + [0xE6A5, 'c'], [0xE6D7, 'W'], [0xE6E0, 'R'], [0xE6F1, 'T'], [0xE6F3, 'a'], [0xE6F5, 'g'], + [0xE705, 'w'], [0xE71A, '3'], [0xE735, 'Z'], [0xE74F, 'Q'], [0xE762, 'r'], [0xE765, 'n'], + [0xE775, 'V'], [0xE77A, 'd'], [0xE77D, 'L'], [0xE77E, '4'], [0xE7C7, 'U'], [0xE7E5, '0'], + [0xE7F6, '7'], [0xE902, 'A'], [0xE915, 'O'], [0xE91F, 'e'], [0xE946, 'a'], [0xE95D, '2'], + [0xE97B, 'f'], [0xE9A8, 'y'], [0xE9CC, 'P'], [0xE9D5, 'o'], [0xE9D7, 'r'], [0xE9F8, 'O'], + [0xE9F9, 'K'], [0xEA15, 'e'], [0xEA20, 'Y'], [0xEA24, 'N'], [0xEA2D, 'v'], [0xEA2E, 'R'], + [0xEA2F, 'C'], [0xEA43, '4'], [0xEA47, 'l'], [0xEA65, 'S'], [0xEA75, 'M'], [0xEA76, 'H'], + [0xEA77, 'u'], [0xEA82, 'o'], [0xEAA1, 'k'], [0xEAA4, 'a'], [0xEAA5, 'x'], [0xEAA6, 'z'], + [0xEAB2, '6'], [0xEAB4, 't'], [0xEABB, 'y'], [0xEAC5, 'w'], [0xEACF, 'b'], [0xEAD5, 'L'], + [0xEAE3, 'A'], [0xEAED, 'F'], [0xEB02, 's'], [0xEB06, 's'], [0xEB0E, 'C'], [0xEB0F, 'R'], + [0xEB18, 'w'], [0xEB27, 'D'], [0xEB62, 'l'], [0xEB63, '9'], [0xEB75, 'h'], [0xEB85, 'X'], + [0xEBEC, 'k'], [0xEBF6, 'N'], [0xEC0F, 'q'], [0xEC19, 'J'], [0xEC50, '7'], [0xEC6D, 'g'], + [0xEC75, 'd'], [0xEC85, 'n'], [0xECAD, 'V'], [0xECB4, 'S'], [0xECD4, 'L'], [0xECDB, 'Z'], + [0xECE6, 'E'], [0xECF8, 'U'], [0xED07, 'V'], [0xED2C, 'Q'], [0xED35, 'l'], [0xED37, 'J'], + [0xED48, 'W'], [0xED64, '5'], [0xED71, '2'], [0xED72, 'v'], [0xED8C, 'E'], [0xEDEB, 'Y'], + [0xEDEC, '5'], [0xEDED, 'm'], [0xEE01, 'c'], [0xEE09, 'Q'], [0xEE0C, 'n'], [0xEE0F, 'u'], + [0xEE47, 'W'], [0xEE5C, 'P'], [0xEE69, 'b'], [0xEE8D, '0'], [0xEEA1, 'X'], [0xEEBB, 'F'], + [0xEEC1, 'I'], [0xEECC, 'B'], [0xEECF, 'c'], [0xEEDA, '1'], [0xEEDB, 'D'], [0xEEE3, 'G'], + [0xEF1F, '8'], [0xEF26, 'K'], [0xEF35, 'x'], [0xEF37, '6'], [0xEF3A, 'd'], [0xEF57, 'H'], + [0xEF5A, 'U'], [0xEF61, 'G'], [0xEF91, '8'], [0xEF94, 'T'], [0xEFC8, 'm'], [0xEFD4, '1'], + [0xEFD7, 'Z'], [0xEFDA, 'h'], [0xEFEE, '3'], [0xEFEF, '4'], [0xEFF6, '3'], [0xF00A, 'q'], + [0xF019, 'T'], [0xF050, 'B'], [0xF065, '0'], [0xF073, '7'], [0xF096, 'z'], [0xF0A6, 't'], + [0xF0BA, 'r'], [0xF0BD, 'M'], [0xF0C0, 'g'], [0xF7A0, '0'], [0xF7A1, '1'], [0xF7A2, '2'], + [0xF7A3, '3'], [0xF7A4, '4'], [0xF7A5, '5'], [0xF7A6, '6'], [0xF7A7, '7'], [0xF7A8, '8'], + [0xF7A9, '9'], [0xF8FF, '*'], +] as [number, string][]).forEach(([code, ch]) => { + GLYPH_MAP[String.fromCharCode(code)] = ch; +}); + +export function decodeGlyphs(text: string): string { + let out = ''; + for (const ch of text) { + out += GLYPH_MAP[ch] ?? ch; + } + return out; +} diff --git a/plugins/vietnamese/SangTacViet/index.ts b/plugins/vietnamese/SangTacViet/index.ts index 98bcca52..544fba93 100644 --- a/plugins/vietnamese/SangTacViet/index.ts +++ b/plugins/vietnamese/SangTacViet/index.ts @@ -4,11 +4,13 @@ import { load as parseHTML } from 'cheerio'; import { fetchApi, fetchText } from '@libs/fetch'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; -import { FilterTypes, Filters } from '@libs/filterInputs'; import { defaultCover } from '@libs/defaultCover'; import { storage } from '@libs/storage'; import { get, set, setFromResponse, removeSessionCookies } from '@libs/cookie'; -import { decodeHtmlEntities, encodeHtmlEntities } from '@libs/utils'; +import { decodeHtmlEntities } from '@libs/utils'; +import { ABT_HOSTS, looksLikeExternalUrl, detectHostFromUrl } from './external-url'; +import { filters } from './filters'; +import { normalizeChapterHtml, wrapWithParagraphs } from './content-normalization'; const SITE = 'https://sangtacviet.app'; @@ -22,325 +24,6 @@ const DOMAIN_URLS = [ ]; const DOMAINS = Object.fromEntries(DOMAIN_URLS.map(u => [new URL(u).host, u])); -// ── External-URL -const HOST_PATTERNS: Record = { - uukanshu: [ - 'uukanshu\\.(?:com|net)/b/(\\d+)/(\\d+)?(.html)?', - 'sj\\.uukanshu\\.(?:com|net)//?book(?:_amp)?\\.aspx\\?id=(\\d+)', - 'sj\\.uukanshu\\.(?:com|net)//?read\\.aspx?tid=(\\d+)&sid=(\\d+)', - 'zhaoshuyuan\\.(?:com|net)/b/(\\d+)/(\\d+)?(.html)?', - 'zhaoshuyuan\\.(?:com|net)//?book(?:_amp)?\\.aspx\\?id=(\\d+)', - 'zhaoshuyuan\\.(?:com|net)//?read\\.aspx?tid=(\\d+)&sid=(\\d+)', - ], - '69shu': [ - '69shuba\\.com/(?:txt|book/)?(\\d+)/?(\\d+)?', - '69shuba\\.cx/(?:txt|book/)?(\\d+)/?(\\d+)?', - '69xinshu\\.com/(?:txt|book/)?(\\d+)/?(\\d+)?', - '69shu\\.pro/(?:txt|book/)?(\\d+)/?(\\d+)?', - '69shu\\.[a-z]{3,4}/(?:txt|book/)?(\\d+)/?(\\d+)?', - '69shuba\\.[a-z]{3,4}/(?:txt|book/)?(\\d+)/?(\\d+)?', - ], - '69shuorg': ['69shu\\.org/book[_/](\\d+)/(\\d+)?'], - xiaoqiangwx: ['xiaoqiangwx\\.org/(?:\\d+|book)/(\\d+)/?(\\d+)?(.html)?'], - cuiweijux: ['cuiweijux\\.com/files/article/html/\\d+/(\\d+)/(\\d+)?(.html)?'], - biquge: [ - 'biquge\\.com\\.cn/book/(\\d+)/(\\d+)?(.html)?', - 'sobiquge\\.com/book/(\\d+)/', - '81zw\\.org/books/(\\d+)/', - ], - trxs: ['trxs\\.cc/tongren/(\\d+)/?(\\d+)?(.html)?'], - ikshu8: ['ikshu8\\.com/book/(\\d+)/?(\\d+)?(.html)?'], - shulinw: [ - 'shulinw\\.com/(?:shu/|yuedu/|book/|\\d+/|modules/article/articleinfo\\.php\\?id=)(\\d+)(?:/)?(\\d*)(.html)?', - ], - wuxia1: [ - 'wuxia1\\.com/(?:shu/|yuedu/|book/|\\d+/|modules/article/articleinfo\\.php\\?id=)(\\d+)(?:/)?(\\d*)(.html)?', - ], - shu05: ['shu05\\.com/\\d+[/_](\\d+)/(\\d*)(.html)?'], - kuhu168: ['kuhu168\\.com/\\d+[/_](\\d+)/(\\d*)(.html)?'], - '2kxs': ['2kxs\\.org/\\d+[/_](\\d+)/(\\d*)(.html)?'], - yikanxiaoshuo: ['yikanxiaoshuo\\.com/\\d+[/_](\\d+)/(\\d*)(.html)?'], - '8zwdu': ['8zwdu\\.com/\\d+[/_](\\d+)/(\\d*)(.html)?'], - kanmaoxian: ['kanmaoxian\\.com/(?:book|\\d+)/(\\d+)/?(\\d*)(.html)?'], - kayegenet: ['kayege\\.net/(?:book|\\d+)[/_](\\d+)/?(\\d*)(.html)?'], - '4gxsw': ['4gxsw\\.com/(?:book|html/\\d+)/(\\d+)/?(\\d*)(.html)?'], - qinqinxsw: ['qinqinxsw\\.com/(?:book|\\d+)[/_](\\d+)/?(\\d*)(.html)?'], - read8: ['read8\\.net/(?:dushu)/(\\d+)/(\\d*)(.html)?'], - ciweimao: ['ciweimao\\.com/book/(\\d+)'], - wkkshu: ['wkkshu\\.com/(?:xs/\\d+/|\\d+_)(\\d+)/(\\d*)(.html)?'], - '168kanshu': ['168kanshu\\.com/(?:xs/\\d+/|\\d+_)(\\d+)/(\\d*)(.html)?'], - wanbentxt: ['wanbentxt\\.com/(\\d+)/([\\d_]*)(.html)?'], - '38kanshu': ['mijiashe\\.com/(\\d+)/([\\d_]*)(.html)?'], - duanqingsi: ['duanqingsi\\.com/(\\d+)/([\\d_]*)(.html)?'], - faloo: [ - '\\.faloo\\.com/[pfboklithtm]+/(\\d+)(?:\\.html|/(\\d+)\\.html)?', - '\\.faloo\\.com/(\\d+)\\.html', - '\\.faloo\\.com/(\\d+)_(\\d+)\\.html', - ], - qiuxiaoshuo: ['qiuxiaoshuo\\.com/(?:book|read)[/-](\\d+)[/-]?(\\d*)'], - dibaqu123: ['dibaqu123\\.com/\\d+/(\\d+)/?(\\d*)(.html)?'], - jiacuan: ['jiacuan\\.com/\\d+/(\\d+)/(\\d*)(.html)?'], - shubaow: ['shubaow\\.net/\\d+[_/](\\d+)/(\\d*)(.html)?'], - biqugeinfo: ['biquge\\.info/\\d+_(\\d+)/(\\d*)(.html)?'], - shumilou: ['shumilou\\.net/\\d+/(\\d+)/(\\d*)(.html)?'], - xbiquge: ['xbiquge\\.cc/book/(\\d+)/(\\d*)(.html)?'], - paoshu8: ['paoshu8\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], - duokan8: ['duokan8\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], - biqugecom: ['biquge\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], - hetushu: ['hetushu\\.com/book/(\\d+)/(\\d*)(.html)?'], - nofff: ['nofff\\.com/(\\d+)/(\\d*)/?'], - uuxs: ['uuxs\\.tw/ls/\\d+_(\\d+)/(\\d*)(.html)?'], - ranwenla: ['ranwen\\.la/files/article/\\d+/(\\d+)/(\\d*)(.html)?'], - '66wx': ['66wx\\.com/(\\d+)_\\d+/read(\\d*)(.html)?'], - biqugexs: ['biqugexs\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], - '230book': ['230book\\.[comnet]{3}/book/(\\d+)/(\\d*)(.html)?'], - biqubu: ['biqubu\\.com/book_(\\d+)/(\\d*)(.html)?'], - '521danmei': [ - '521danmei\\.org/read/(\\d+)/(\\d*)/?', - '521danmei\\.org/book/(\\d+)\\.html', - ], - bxwxorg: [ - 'bxwxorg\\.com/read/(\\d+)/(\\d*)(.html)?', - 'bxwxorg\\.com/book/(\\d+)\\.html', - ], - qidian: ['qidian\\.com/(?:book|info)/(\\d+)'], - zwdu: ['zwdu\\.com/book/(\\d+)/(\\d*)(.html)?'], - zongheng: [ - 'book\\.zongheng\\.com/chapter/(\\d+)/(\\d*)(.html)?', - 'book\\.zongheng\\.com/book/(\\d+)(?:\\.html)?', - ], - biqugese: [ - 'biquge\\.se/(\\d+)/(\\d*)(.html)?', - 'biqugse\\.com/(\\d+)/(\\d*)(.html)?', - ], - qiushubang: ['qiushubang\\.com/(\\d+)/(\\d*)(.html)?'], - xinshuhaige: ['xinshuhaige\\.com/(\\d+)/(\\d*)(.html)?'], - oldtimescc: ['oldtimescc\\.cc/go/(\\d+)/(\\d*)(.html)?'], - wuwuxs: ['wuwuxs\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], - hs313: ['hs313\\.net/book/(\\d+)/(\\d*)(.html)?'], - shuchong: ['shuchong\\.info/chapter/(\\d+)/(\\d*)(.html)?'], - shucw: ['shucw\\.com/html/\\d+/(\\d+)/(\\d*)(.html)?'], - shumizu: ['shumizu\\.com/\\d+/(\\d+)/(\\d*)(.html)?'], - tadu: ['tadu\\.com/book/(\\d+)/?(\\d*)/?'], - ptwxz: [ - 'ptwxz\\.com/bookinfo/\\d+/(\\d+)\\.html', - 'ptwxz\\.com/html/\\d+/(\\d+)/(\\d+)\\.html', - 'ptwxz\\.com/html/\\d+/(\\d+)/', - ], - x81zw: ['x81zw\\.com/book/\\d+/(\\d+)/(\\d*)(.html)?'], - linovel: ['linovel\\.net/book/(\\d+)\\.html'], - wenku8: ['wenku8\\.net/novel/\\d+/(\\d+)/(\\d*)\\.htm'], - youyoukanshu: [ - 'youyoukanshu\\.com/book/(\\d+)\\.html', - 'youyoukanshu\\.com/book/(\\d+)/(\\d*)(.html)?', - ], - biqubao: ['biqubao\\.com/book/(\\d+)/(\\d*)(.html)?'], - biqugele: ['biqugele\\.com/txt/(\\d+)/(\\d*)(.html)?'], - biqugebz: ['biquge\\.bz/(\\d+)/(\\d*)(.html)?'], - biquge5200: ['biquge5200\\.cc/\\d+_(\\d+)/(\\d*)(.html)?'], - sfacg: [ - 'sfacg\\.com/(?:Novel|b|i)/(\\d+)/?', - 'sfacg\\.com/Novel/(\\d+)/MainIndex/', - ], - shubao45: ['shubao45\\.com/\\d+_(\\d+)/(\\d*)(.html)?'], - kujiang: ['kujiang\\.com/book/(\\d+)'], - yushubo: [ - 'yushubo\\.com/book_(\\d+)', - 'yushubo\\.net/book_(\\d+)', - 'yushugu\\.com/book_(\\d+)', - 'yushugu\\.com/list_other_(\\d+)', - 'yushugu\\.com/read_(\\d+)_(\\d+)\\.html', - ], - xklxsw: ['xklxsw\\.com/book/(\\d+)/'], - fanqie: ['fanqienovel\\.com/page/(\\d+)'], - xsbiquge: ['xsbiquge\\.net/\\d+_(\\d+)/(\\d*)(.html)?'], - jjwxc: [ - 'jjwxc\\.net//?onebook\\.php\\?novelid=(\\d+)', - 'jjwxc\\.net//?book2/(\\d+)/?', - ], - qimao: ['qimao\\.com/shuku/(\\d+)/'], - ddxs: ['ddxs\\.com/([a-z0-9A-Z\\-_]+)/(\\d+)?'], - quanben5: ['quanben5\\.com/n/([a-z0-9A-Z\\-_]+)/'], - idejian: ['idejian\\.com/book/([0-9]+)/'], -}; - -// Hosts whose first capture is alphanumeric and must be resolved to a numeric -// bookid via /index.php?sajax=tryaddabtrecord (mirrors `isAbtHost()`). -const ABT_HOSTS = new Set(['ddxs', 'quanben5', 'colamanga']); - -function looksLikeExternalUrl(s: string): boolean { - const t = s.toLowerCase(); - return t.startsWith('http://') || t.startsWith('https://'); -} - -function detectHostFromUrl( - input: string, -): { host: string; bookid: string; chapterid?: string } | null { - for (const host in HOST_PATTERNS) { - for (const pat of HOST_PATTERNS[host]) { - const m = new RegExp(pat, 'i').exec(input); - if (!m) continue; - if (m[2] !== undefined && m[2] !== m[1] && /^[\d_\-]+$/.test(m[2])) { - if (m[1]) return { host, bookid: m[1], chapterid: m[2] }; - } - if (m[1]) return { host, bookid: m[1] }; - } - } - return null; -} - -// ── Webfont glyph decode ───────────────────────────── -const GLYPH_MAP: Record = {}; -// prettier-ignore -([ - [0xE01B, 'A'], [0xE01E, 'y'], [0xE05F, '3'], [0xE063, 'z'], [0xE06B, 'K'], [0xE06C, 't'], - [0xE089, 'l'], [0xE0D5, 'S'], [0xE0D6, 'T'], [0xE100, 'o'], [0xE101, 'P'], [0xE116, '4'], - [0xE122, 'W'], [0xE124, 'Z'], [0xE14B, 'J'], [0xE160, 'e'], [0xE184, 'O'], [0xE186, 'D'], - [0xE1A4, 'f'], [0xE1AD, 'e'], [0xE1B4, 'k'], [0xE1B8, 'f'], [0xE1BF, 'n'], [0xE1C0, 'Y'], - [0xE1C1, '1'], [0xE1D8, 'K'], [0xE1E4, 'M'], [0xE1EA, 'Y'], [0xE215, 'C'], [0xE218, 'A'], - [0xE22B, 'h'], [0xE240, 'x'], [0xE248, 'v'], [0xE257, 'G'], [0xE27E, 'b'], [0xE2A9, 'B'], - [0xE2C5, 's'], [0xE2C7, 't'], [0xE2CA, 'G'], [0xE2E3, 'k'], [0xE2F8, 'q'], [0xE30F, 'F'], - [0xE311, 'u'], [0xE32F, 'E'], [0xE334, '2'], [0xE34A, 'I'], [0xE37C, 'R'], [0xE38F, 'v'], - [0xE39B, 'X'], [0xE3B0, 'l'], [0xE3B7, '7'], [0xE3F1, 'l'], [0xE41B, 'o'], [0xE41C, 'H'], - [0xE426, 'S'], [0xE427, 'J'], [0xE43E, '6'], [0xE44E, 'X'], [0xE46A, 'b'], [0xE477, 'y'], - [0xE49A, 'c'], [0xE4A3, '8'], [0xE4AE, '2'], [0xE4CC, 's'], [0xE4D3, '5'], [0xE4DB, 'L'], - [0xE4DF, 'N'], [0xE4EC, '5'], [0xE4F3, 'r'], [0xE519, '0'], [0xE51F, 'g'], [0xE550, 'E'], - [0xE557, 'h'], [0xE566, 'N'], [0xE571, 'F'], [0xE57B, 'O'], [0xE5BD, 'C'], [0xE5C1, 'd'], - [0xE5C9, '8'], [0xE5D1, 'x'], [0xE5DC, 'm'], [0xE5E1, '9'], [0xE5F0, 'u'], [0xE5FA, 'm'], - [0xE5FF, 'a'], [0xE603, 'U'], [0xE62A, 'w'], [0xE636, 'P'], [0xE63E, 'D'], [0xE648, '6'], - [0xE65B, 'H'], [0xE65D, 'z'], [0xE660, '9'], [0xE68D, '1'], [0xE691, 'M'], [0xE6A4, 'q'], - [0xE6A5, 'c'], [0xE6D7, 'W'], [0xE6E0, 'R'], [0xE6F1, 'T'], [0xE6F3, 'a'], [0xE6F5, 'g'], - [0xE705, 'w'], [0xE71A, '3'], [0xE735, 'Z'], [0xE74F, 'Q'], [0xE762, 'r'], [0xE765, 'n'], - [0xE775, 'V'], [0xE77A, 'd'], [0xE77D, 'L'], [0xE77E, '4'], [0xE7C7, 'U'], [0xE7E5, '0'], - [0xE7F6, '7'], [0xE902, 'A'], [0xE915, 'O'], [0xE91F, 'e'], [0xE946, 'a'], [0xE95D, '2'], - [0xE97B, 'f'], [0xE9A8, 'y'], [0xE9CC, 'P'], [0xE9D5, 'o'], [0xE9D7, 'r'], [0xE9F8, 'O'], - [0xE9F9, 'K'], [0xEA15, 'e'], [0xEA20, 'Y'], [0xEA24, 'N'], [0xEA2D, 'v'], [0xEA2E, 'R'], - [0xEA2F, 'C'], [0xEA43, '4'], [0xEA47, 'l'], [0xEA65, 'S'], [0xEA75, 'M'], [0xEA76, 'H'], - [0xEA77, 'u'], [0xEA82, 'o'], [0xEAA1, 'k'], [0xEAA4, 'a'], [0xEAA5, 'x'], [0xEAA6, 'z'], - [0xEAB2, '6'], [0xEAB4, 't'], [0xEABB, 'y'], [0xEAC5, 'w'], [0xEACF, 'b'], [0xEAD5, 'L'], - [0xEAE3, 'A'], [0xEAED, 'F'], [0xEB02, 's'], [0xEB06, 's'], [0xEB0E, 'C'], [0xEB0F, 'R'], - [0xEB18, 'w'], [0xEB27, 'D'], [0xEB62, 'l'], [0xEB63, '9'], [0xEB75, 'h'], [0xEB85, 'X'], - [0xEBEC, 'k'], [0xEBF6, 'N'], [0xEC0F, 'q'], [0xEC19, 'J'], [0xEC50, '7'], [0xEC6D, 'g'], - [0xEC75, 'd'], [0xEC85, 'n'], [0xECAD, 'V'], [0xECB4, 'S'], [0xECD4, 'L'], [0xECDB, 'Z'], - [0xECE6, 'E'], [0xECF8, 'U'], [0xED07, 'V'], [0xED2C, 'Q'], [0xED35, 'l'], [0xED37, 'J'], - [0xED48, 'W'], [0xED64, '5'], [0xED71, '2'], [0xED72, 'v'], [0xED8C, 'E'], [0xEDEB, 'Y'], - [0xEDEC, '5'], [0xEDED, 'm'], [0xEE01, 'c'], [0xEE09, 'Q'], [0xEE0C, 'n'], [0xEE0F, 'u'], - [0xEE47, 'W'], [0xEE5C, 'P'], [0xEE69, 'b'], [0xEE8D, '0'], [0xEEA1, 'X'], [0xEEBB, 'F'], - [0xEEC1, 'I'], [0xEECC, 'B'], [0xEECF, 'c'], [0xEEDA, '1'], [0xEEDB, 'D'], [0xEEE3, 'G'], - [0xEF1F, '8'], [0xEF26, 'K'], [0xEF35, 'x'], [0xEF37, '6'], [0xEF3A, 'd'], [0xEF57, 'H'], - [0xEF5A, 'U'], [0xEF61, 'G'], [0xEF91, '8'], [0xEF94, 'T'], [0xEFC8, 'm'], [0xEFD4, '1'], - [0xEFD7, 'Z'], [0xEFDA, 'h'], [0xEFEE, '3'], [0xEFEF, '4'], [0xEFF6, '3'], [0xF00A, 'q'], - [0xF019, 'T'], [0xF050, 'B'], [0xF065, '0'], [0xF073, '7'], [0xF096, 'z'], [0xF0A6, 't'], - [0xF0BA, 'r'], [0xF0BD, 'M'], [0xF0C0, 'g'], [0xF7A0, '0'], [0xF7A1, '1'], [0xF7A2, '2'], - [0xF7A3, '3'], [0xF7A4, '4'], [0xF7A5, '5'], [0xF7A6, '6'], [0xF7A7, '7'], [0xF7A8, '8'], - [0xF7A9, '9'], [0xF8FF, '*'], -] as [number, string][]).forEach(([code, ch]) => { - GLYPH_MAP[String.fromCharCode(code)] = ch; -}); - -function decodeGlyphs(text: string): string { - let out = ''; - for (const ch of text) { - out += GLYPH_MAP[ch] ?? ch; - } - return out; -} - -// ── Content normalization ───────────────── -function looksLikeInterlinear(text: string): boolean { - return /]*\b(?:t|v|p)\s*=\s*['"][^'"]*['"][^>]*>/i.test(text); -} - -function normalizeInterlinear(raw: string): string { - let t = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - t = t.replace(/\s*/gi, '\n').replace(/<\/br\s*>/gi, '\n'); - t = t.replace( - /<\/i>\s*\n+\s*(?=[,.;:!?%\)\]\}\u3002\uff0c\u3001\uff01\uff1f\uff1b\uff1a\u201d\u2019\u300d\u300f\u3011\u300b])/gi, - '', - ); - const marker = '__STV_GAP__'; - t = t.replace(/<\/i>\s*' + marker + ']*>([\s\S]*?)<\/i>/gi, '$1'); - t = t.replace( - /<\/?(p|div|article|section|li|tr|h[1-6]|blockquote|ul|ol)[^>]*>/gi, - '\n', - ); - t = t.replace(/<[^>]+>/g, '').replace(/[<>]/g, ''); - t = decodeHtmlEntities(t); - t = t.replace(new RegExp(marker, 'g'), ' '); - t = t.replace(/[\t\f\v ]+/g, ' '); - t = t.replace( - / +([,.;:!?%\)\]\}\u3002\uff0c\u3001\uff01\uff1f\uff1b\uff1a\u201d\u2019\u300d\u300f\u3011\u300b])/g, - '$1', - ); - t = t.replace( - /\n+([,.;:!?%\)\]\}\u3002\uff0c\u3001\uff01\uff1f\uff1b\uff1a\u201d\u2019\u300d\u300f\u3011\u300b])/g, - '$1', - ); - t = t - .replace(/[ ]*\n[ ]*/g, '\n') - .replace(/\n{3,}/g, '\n\n') - .trim(); - return t ? t.replace(/\n/g, '
') : ''; -} - -function normalizeChapterHtml(host: string, raw: string): string { - let text = raw || ''; - if (!text) return ''; - const h = host.toLowerCase(); - - if (h === 'fanqie') { - text = text.replace(/]*>[\s\S]*?<\/header>/gi, ''); - text = text.replace(/]*>[\s\S]*?<\/footer>/gi, ''); - text = text.replace(/<\/?article>/gi, ''); - text = text.replace(/\sidx="\d+"/g, ''); - if (looksLikeInterlinear(text)) return normalizeInterlinear(text); - return text; - } - - if (h === 'sangtac' || h === 'dich') { - text = decodeGlyphs(text); - if (looksLikeInterlinear(text)) return normalizeInterlinear(text); - if (!/<\w+[^>]*>/.test(text)) { - text = text - .replace(/&/g, '&') - .replace(//g, '>'); - text = text - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n') - .replace(/\n/g, '
'); - } - return text; - } - - if (!/<\w+[^>]*>/.test(text)) { - text = text - .replace(/&/g, '&') - .replace(//g, '>'); - text = text - .replace(/\r\n/g, '\n') - .replace(/\r/g, '\n') - .replace(/\n/g, '
'); - return text; - } - if (looksLikeInterlinear(text)) return normalizeInterlinear(text); - return text; -} - -function wrapWithParagraphs(rawText: string): string { - if (!rawText) return ''; - const cleanText = rawText.replace(//gi, '\n'); - const paragraphs = cleanText.split('\n'); - const htmlResult = paragraphs - .map(line => line.trim()) - .filter(line => line.length > 0) - .map(line => `

${encodeHtmlEntities(line)}

`) - .join('\n'); - return htmlResult; -} - class STVChapterError extends Error { public errorCode: number; public raw: any; @@ -356,8 +39,8 @@ class STVChapterError extends Error { } get shouldStopRetry(): boolean { switch (this.errorCode) { - case 0: // OK - case 1: // empty + case 0: + case 1: case 12: case 13: case 15: @@ -411,11 +94,12 @@ class SangTacVietPlugin implements Plugin.PluginBase { name = 'Sáng Tác Việt'; icon = 'src/vi/sangtacviet/icon.png'; customJS = 'src/vi/sangtacviet/custom.js'; + private _captchaVerifiedPath: string | null = null; get site() { return DOMAINS[this.selectedDomain] || SITE; } - version = '1.0.20'; + version = '1.0.23'; webStorageUtilized = true; pluginSettings: Plugin.PluginSettings = { @@ -784,39 +468,47 @@ class SangTacVietPlugin implements Plugin.PluginBase { const origin = new URL(this.site).origin; - // Step 0: clear session cookies but preserve translation preference. - const oldCookies = await get(origin); - for (const k in oldCookies) { - await set(origin, { name: k, value: '' }); - } - await removeSessionCookies(); + // If this is a retry after captcha verification (refetch from custom.js), + // skip Steps 0+1 to preserve the captcha-verified session. + const isPostCaptcha = this._captchaVerifiedPath === chapterPath; + this._captchaVerifiedPath = null; // consume the flag - // Set translation cookies BEFORE Step 1 so the server sees them - // on the very first request (session creation). - await this.applyTranslationCookies(origin); + if (!isPostCaptcha) { + // Step 0: clear session cookies but preserve translation preference. + const existingJar = await get(origin); + for (const k in existingJar) { + await set(origin, { name: k, value: '' }); + } + await removeSessionCookies(); - // Step 1: prime the session — sends translation cookies with the GET. - try { - const pageRes = await fetchApi(referer, { - headers: { ...(await this._cookieHeader(origin)) }, - }); - const html = await pageRes.text(); - const firstCookies = this.extractCookiesFromHtml(html); - for (const k in firstCookies) { - await set(origin, { - name: k, - value: firstCookies[k], + // Set translation cookies BEFORE Step 1 so the server sees them + // on the very first request (session creation). + await this.applyTranslationCookies(origin); + await set(origin, { name: 'cookieenabled', value: 'true' }); + + // Step 1: prime the session — sends translation cookies with the GET. + try { + const pageRes = await fetchApi(referer, { + headers: { ...(await this._cookieHeader(origin)) }, }); + const html = await pageRes.text(); + const firstCookies = this.extractCookiesFromHtml(html); + for (const k in firstCookies) { + await set(origin, { + name: k, + value: firstCookies[k], + }); + } + if (pageRes.headers.get('set-cookie')) { + await setFromResponse(origin, pageRes.headers.get('set-cookie')!); + } + } catch { + // continue — server may still recognise the WebView's cookie jar } - if (pageRes.headers.get('set-cookie')) { - await setFromResponse(origin, pageRes.headers.get('set-cookie')!); - } - } catch { - // continue — server may still recognise the WebView's cookie jar - } - // Re-apply translation cookies in case Step 1's Set-Cookie overwrote them. - await this.applyTranslationCookies(origin); + // Re-apply translation cookies in case Step 1's Set-Cookie overwrote them. + await this.applyTranslationCookies(origin); + } const apiUrl = new URL(`${this.site}/index.php`); apiUrl.searchParams.set('bookid', bookId); @@ -827,43 +519,73 @@ class SangTacVietPlugin implements Plugin.PluginBase { apiUrl.searchParams.set('sty', '1'); apiUrl.searchParams.set('exts', ''); - // Step 2: probe POST that rotates `_ac`. - // The browser sends the current `_ac` cookie value as the POST body. - // Sending an empty body triggers the anti-bot heuristic (code 21). - try { - const jar = await get(origin); - const acValue = - (jar._ac && (typeof jar._ac === 'string' ? jar._ac : jar._ac.value)) || - ''; - const probeRes = await fetchApi(apiUrl.toString(), { + // Steps 2+3: probe + fetch in a retry loop (same session). + // The JS reference retries up to 10 times within the same session, + // sleeping 1s between attempts when the server returns code 5/7. + const maxInnerRetries = this.autoRetry ? 10 : 1; + let data: ReturnType = null; + for (let attempt = 0; attempt < maxInnerRetries; attempt++) { + // Step 2: probe POST that rotates `_ac`. + try { + const jar = await get(origin); + const acValue = + (jar._ac && + (typeof jar._ac === 'string' ? jar._ac : jar._ac.value)) || + ''; + const probeRes = await fetchApi(apiUrl.toString(), { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'X-Requested-With': 'XmlHttpRequest', + Origin: origin, + Referer: referer, + ...(await this._cookieHeader(origin)), + }, + body: acValue, + }); + await probeRes.text(); + if (probeRes.headers.get('set-cookie')) { + await setFromResponse(origin, probeRes.headers.get('set-cookie')!); + } + } catch { + // probe is best-effort; carry on + } + + // Step 3: actual chapter fetch using the rotated cookies. + const chapRes = await fetchApi(apiUrl.toString(), { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'X-Requested-With': 'XmlHttpRequest', + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Origin: origin, Referer: referer, ...(await this._cookieHeader(origin)), }, - body: acValue, + body: '', }); - await probeRes.text(); - if (probeRes.headers.get('set-cookie')) { - await setFromResponse(origin, probeRes.headers.get('set-cookie')!); + if (chapRes.headers.get('set-cookie')) { + await setFromResponse(origin, chapRes.headers.get('set-cookie')!); + } + data = this.parseLooseJson(await chapRes.text()); + const code = data && String(data.code); + if (code === '0') break; + // Stop codes: don't retry + if ( + code === '1' || + code === '12' || + code === '13' || + code === '15' || + code === '18' || + code === '19' || + code === '21' || + code === '101' + ) { + break; + } + // code 5/7 etc → retry after 1s + if (attempt < maxInnerRetries - 1) { + await new Promise(res => setTimeout(res, 1000)); } - } catch { - // probe is best-effort; carry on } - - // Step 3: actual chapter fetch using the rotated cookies. - const chapRes = await fetchApi(apiUrl.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Referer: referer, - ...(await this._cookieHeader(origin)), - }, - body: '', - }); - const data = this.parseLooseJson(await chapRes.text()); if (!data) { throw new Error( 'Bạn đang tải chương quá nhanh. Hãy thử lại sau vài giây.\nReason: null', @@ -884,6 +606,7 @@ class SangTacVietPlugin implements Plugin.PluginBase { console.warn('Unexpected chapter API response', data); switch (String(data.code)) { case '21': { + this._captchaVerifiedPath = chapterPath; return "
"; } default: { @@ -960,131 +683,7 @@ class SangTacVietPlugin implements Plugin.PluginBase { return this.site + path; } - filters = { - sort: { - type: FilterTypes.Picker, - label: 'Sắp xếp', - value: 'view', - options: [ - { label: 'Không sắp xếp', value: '' }, - { label: 'Mới nhập kho', value: 'new' }, - { label: 'Mới cập nhật', value: 'update' }, - { label: 'Lượt đọc tổng', value: 'view' }, - { label: 'Lượt đọc tuần', value: 'viewweek' }, - { label: 'Lượt đọc ngày', value: 'viewday' }, - { label: 'Lượt thích', value: 'like' }, - { label: 'Lượt theo dõi', value: 'following' }, - { label: 'Lượt đánh dấu', value: 'bookmarked' }, - { label: 'Đề cử', value: 'auto' }, - ], - }, - minc: { - type: FilterTypes.Picker, - label: 'Số chương tối thiểu', - value: '0', - options: [ - { label: 'Tất cả', value: '0' }, - { label: '> 50', value: '50' }, - { label: '> 100', value: '100' }, - { label: '> 200', value: '200' }, - { label: '> 500', value: '500' }, - { label: '> 1000', value: '1000' }, - { label: '> 1500', value: '1500' }, - { label: '> 2000', value: '2000' }, - ], - }, - category: { - type: FilterTypes.Picker, - label: 'Thể loại chính', - value: '', - options: [ - { label: 'Tất cả', value: '' }, - { label: 'Huyền huyễn', value: 'hh' }, - { label: 'Đô thị', value: 'dt' }, - { label: 'Ngôn tình', value: 'nt' }, - { label: 'Võng du', value: 'vd' }, - { label: 'Khoa học viễn tưởng', value: 'kh' }, - { label: 'Lịch sử', value: 'ls' }, - { label: 'Đồng nhân', value: 'dn' }, - { label: 'Dị năng', value: 'dna' }, - { label: 'Linh dị', value: 'ld' }, - { label: 'Light Novel', value: 'ln' }, - ], - }, - type: { - type: FilterTypes.Picker, - label: 'Loại truyện', - value: '', - options: [ - { label: 'Tất cả', value: '' }, - { label: 'Truyện sáng tác', value: 'sangtac' }, - { label: 'Truyện dịch', value: 'dich' }, - { label: 'Truyện tranh', value: 'comic' }, - { label: 'Txt dịch tự động', value: 'txt' }, - { label: 'Truyện scan ảnh', value: 'scan' }, - ], - }, - step: { - type: FilterTypes.Picker, - label: 'Trạng thái đăng', - value: '', - options: [ - { label: 'Tất cả', value: '' }, - { label: 'Hoàn thành', value: '3' }, - { label: 'Còn tiếp', value: '1' }, - { label: 'Tạm ngưng', value: '2' }, - { label: 'Không tạm ngưng', value: '5' }, - ], - }, - host: { - type: FilterTypes.Picker, - label: 'Nguồn truyện', - value: '', - options: [ - { label: 'Tất cả', value: '' }, - { label: '[Vip] fanqie (Cà Chua)', value: 'fanqie' }, - { label: '[Vip] qidian (Khởi Điểm)', value: 'qidian' }, - { label: '[Vip] ciweimao', value: 'ciweimao' }, - { label: '[Vip] faloo (Phi Lư)', value: 'faloo' }, - { label: '[Vip] jjwxc (Tấn Giang)', value: 'jjwxc' }, - { label: '[Vip] sfacg', value: 'sfacg' }, - { label: '[Vip] zongheng (Tung Hoành)', value: 'zongheng' }, - { label: '[Free] 69shu', value: '69shu' }, - { label: '[Free] tadu', value: 'tadu' }, - { label: '[Free] qimao (7 Mèo)', value: 'qimao' }, - { label: '[Free] idejian', value: 'idejian' }, - { label: '[LN] linovel', value: 'linovel' }, - { label: '[LN] wenku8', value: 'wenku8' }, - ], - }, - tag: { - type: FilterTypes.CheckboxGroup, - label: 'Nhãn', - value: [], - options: [ - { label: 'Đô Thị', value: 'dothi' }, - { label: 'Xuyên Qua', value: 'xuyenqua' }, - { label: 'Hệ Thống', value: 'hethong' }, - { label: 'Huyền Huyễn', value: 'huyenhuyen' }, - { label: 'Ngôn Tình', value: 'ngontinh' }, - { label: 'Đồng Nhân', value: 'dongnhan' }, - { label: 'Trùng Sinh', value: 'trungsinh' }, - { label: 'Lịch Sử', value: 'lichsu' }, - { label: 'Khoa Huyễn', value: 'khoahuyen' }, - { label: 'Tiên Hiệp', value: 'tienhiep' }, - { label: 'Võ Hiệp', value: 'vohiep' }, - { label: 'Sảng Văn', value: 'sangvan' }, - { label: 'Light Novel', value: 'lightnovel' }, - { label: 'Linh Dị', value: 'linhdi' }, - { label: 'Kỳ Huyễn', value: 'kyhuyen' }, - { label: 'Tận Thế', value: 'tanthe' }, - { label: 'Ngọt Sủng', value: 'ngotsung' }, - { label: 'Sân Trường', value: 'santruong' }, - { label: 'Nhiệt Huyết', value: 'nhiethuyet' }, - { label: 'Nhanh Xuyên', value: 'nhanhxuyen' }, - ], - }, - } satisfies Filters; + filters = filters; } export default new SangTacVietPlugin(); diff --git a/plugins/vietnamese/SangTacViet/webview/index.js b/plugins/vietnamese/SangTacViet/webview/index.js index f266bdbd..48efa28a 100644 --- a/plugins/vietnamese/SangTacViet/webview/index.js +++ b/plugins/vietnamese/SangTacViet/webview/index.js @@ -1,191 +1,187 @@ -const captchaHTML = `
-
-
Captcha
-
- -
- Captcha Image -
- - -
-
- - -
`; - -const refreshCaptcha = () => { - const captchaImage = document.getElementById('captcha-image'); - const captchaInput = document.getElementById('captcha-input'); - captchaImage.src = '/generate_captcha.php?random=' + Math.random(); - captchaInput.value = ''; -}; - -const captchaBtnClick = async () => { - const captchaInput = document.getElementById('captcha-input'); - const captchaBtn = document.getElementById('captcha-btn'); - const captchaError = document.getElementById('captcha-error'); - const token = captchaInput.value.trim(); - - if (!token) { - captchaError.textContent = 'Vui lòng nhập mã xác thực!'; - captchaError.style.display = 'block'; - return; + } catch (e) { + console.error('[STV] refetch error:', e); } - - if (token.length < 4) { - captchaError.textContent = 'Mã xác thực phải có ít nhất 4 ký tự!'; - captchaError.style.display = 'block'; - return; +} + +function verifyToken(token, provider) { + return fetch('/index.php?ngmar=verifyca', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: + 'ajax=verifycaptcha&token=' + + encodeURIComponent(token) + + '&purpose=read&provider=' + + encodeURIComponent(provider), + }).then(function (r) { + return r.text(); + }); +} + +// ── Cloudflare Turnstile (auto-solves invisibly) ── + +var TURNSTILE_SITEKEY = '0x4AAAAAABVjME7NHipdnj-c'; +var turnstileTimeout = null; + +function tryTurnstile(placeholder) { + return new Promise(function (resolve) { + // Timeout — fall back to image captcha after 10s + turnstileTimeout = setTimeout(function () { + console.warn('[STV] Turnstile timeout, falling back to image captcha'); + resolve(false); + }, 10000); + + var container = document.createElement('div'); + container.id = 'turnstile-box'; + placeholder.appendChild(container); + + var script = document.createElement('script'); + script.src = + 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; + script.async = true; + + script.onload = function () { + try { + turnstile.render('#turnstile-box', { + sitekey: TURNSTILE_SITEKEY, + callback: function (token) { + clearTimeout(turnstileTimeout); + verifyToken(token, 'cloudflare').then(function (text) { + if (text.trim() === 'success') { + doRefetch(); + resolve(true); + } else { + console.warn('[STV] Turnstile verify failed:', text); + resolve(false); + } + }); + }, + 'error-callback': function () { + clearTimeout(turnstileTimeout); + console.warn('[STV] Turnstile error, falling back'); + resolve(false); + }, + }); + } catch (e) { + clearTimeout(turnstileTimeout); + console.warn('[STV] Turnstile render error:', e); + resolve(false); + } + }; + + script.onerror = function () { + clearTimeout(turnstileTimeout); + console.warn('[STV] Turnstile script failed to load'); + resolve(false); + }; + + document.head.appendChild(script); + }); +} + +// ── Image captcha fallback ── + +var captchaHTML = + '
' + + '
' + + '
Captcha
' + + '
' + + '' + + '
' + + 'Captcha' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '
'; + +function showImageCaptcha(placeholder) { + // Remove any turnstile remnant + var old = document.getElementById('turnstile-box'); + if (old) old.remove(); + + placeholder.innerHTML = captchaHTML; + + var img = document.getElementById('captcha-image'); + var input = document.getElementById('captcha-input'); + var btn = document.getElementById('captcha-btn'); + var err = document.getElementById('captcha-error'); + + function refresh() { + img.src = '/generate_captcha.php?random=' + Math.random(); + input.value = ''; } - captchaBtn.disabled = true; - captchaBtn.textContent = 'Đang kiểm tra...'; - captchaError.style.display = 'none'; - - try { - const urlSearchParams = new URLSearchParams(); - urlSearchParams.append('ajax', 'verifycaptcha'); - urlSearchParams.append('token', token); - urlSearchParams.append('purpose', 'read'); - urlSearchParams.append('provider', 'sangtacviet'); - const response = await fetch('/index.php?ngmar=verifyca', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: urlSearchParams.toString(), - }); - - const text = await response.text(); - - if (text.trim() === 'success') { - try { - if ( - typeof window.reader !== 'undefined' && - typeof window.reader.refetch === 'function' - ) { - window.reader.refetch(); + img.addEventListener('click', refresh); + btn.addEventListener('click', function () { + var token = input.value.trim(); + if (!token || token.length < 4) { + err.textContent = 'Mã xác thực phải có ít nhất 4 ký tự!'; + err.style.display = 'block'; + return; + } + btn.disabled = true; + btn.textContent = 'Đang kiểm tra...'; + err.style.display = 'none'; + + verifyToken(token, 'sangtacviet') + .then(function (text) { + if (text.trim() === 'success') { + doRefetch(); } else { - console.warn('[Captcha] window.reader.refetch không tồn tại.'); + err.textContent = 'Mã xác thực không chính xác, vui lòng thử lại.'; + err.style.display = 'block'; + refresh(); } - } catch (e) { - console.error('[Captcha] Lỗi khi thực thi window.reader.refetch:', e); - } - } else { - captchaError.textContent = - 'Mã xác thực không chính xác, vui lòng thử lại.'; - captchaError.style.display = 'block'; - refreshCaptcha(); + }) + .catch(function () { + err.textContent = 'Đã có lỗi xảy ra khi kết nối tới máy chủ.'; + err.style.display = 'block'; + }) + .finally(function () { + btn.disabled = false; + btn.textContent = 'Xác thực'; + }); + }); + + refresh(); +} + +// ── Entry point ── + +document.addEventListener('DOMContentLoaded', function () { + var placeholder = document.getElementById('captcha-placeholder'); + if (!placeholder) return; + + placeholder.textContent = 'Đang xác thực...'; + + // Try invisible Turnstile first; fall back to image captcha + tryTurnstile(placeholder).then(function (solved) { + if (!solved) { + showImageCaptcha(placeholder); } - } catch (error) { - console.error('[Captcha] Lỗi kết nối mạng:', error); - captchaError.textContent = 'Đã có lỗi xảy ra khi kết nối tới máy chủ.'; - captchaError.style.display = 'block'; - } finally { - captchaBtn.disabled = false; - captchaBtn.textContent = 'Xác thực'; - } -}; - -document.addEventListener('DOMContentLoaded', () => { - const captchaPlaceholder = document.getElementById('captcha-placeholder'); - if (captchaPlaceholder) { - console.log('Detected captcha placeholder, injecting captcha HTML.'); - captchaPlaceholder.innerHTML = captchaHTML; - refreshCaptcha(); - } + }); }); From b144919c7b342647e2813e5ff2ce9fa7255300ef Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Sat, 23 May 2026 14:14:25 +0700 Subject: [PATCH 2/4] Update plugins/vietnamese/SangTacViet/content-normalization.ts --- plugins/vietnamese/SangTacViet/content-normalization.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/vietnamese/SangTacViet/content-normalization.ts b/plugins/vietnamese/SangTacViet/content-normalization.ts index 34c249d7..83b12377 100644 --- a/plugins/vietnamese/SangTacViet/content-normalization.ts +++ b/plugins/vietnamese/SangTacViet/content-normalization.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-useless-escape */ + import { decodeHtmlEntities, encodeHtmlEntities } from '@libs/utils'; import { decodeGlyphs } from './glyph-decode'; From 3c8bd148c8463ea4ebb20a41cc7c59b3513a3d82 Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Sat, 23 May 2026 14:14:31 +0700 Subject: [PATCH 3/4] Update plugins/vietnamese/SangTacViet/index.ts --- plugins/vietnamese/SangTacViet/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/vietnamese/SangTacViet/index.ts b/plugins/vietnamese/SangTacViet/index.ts index 544fba93..fe9eabcc 100644 --- a/plugins/vietnamese/SangTacViet/index.ts +++ b/plugins/vietnamese/SangTacViet/index.ts @@ -99,7 +99,7 @@ class SangTacVietPlugin implements Plugin.PluginBase { get site() { return DOMAINS[this.selectedDomain] || SITE; } - version = '1.0.23'; + version = '1.0.21'; webStorageUtilized = true; pluginSettings: Plugin.PluginSettings = { From e77b3049fb74f6406d314fb704b552d22a8de206 Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Sat, 23 May 2026 14:14:37 +0700 Subject: [PATCH 4/4] Update plugins/vietnamese/SangTacViet/webview/index.js --- plugins/vietnamese/SangTacViet/webview/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/vietnamese/SangTacViet/webview/index.js b/plugins/vietnamese/SangTacViet/webview/index.js index 48efa28a..8535a51d 100644 --- a/plugins/vietnamese/SangTacViet/webview/index.js +++ b/plugins/vietnamese/SangTacViet/webview/index.js @@ -53,7 +53,7 @@ function tryTurnstile(placeholder) { script.onload = function () { try { - turnstile.render('#turnstile-box', { + window.turnstile?.render('#turnstile-box', { sitekey: TURNSTILE_SITEKEY, callback: function (token) { clearTimeout(turnstileTimeout);