补充2
This commit is contained in:
@@ -174,6 +174,9 @@ function renderTemplateExpressions(raw: string, keyword = '', page = 1) {
|
|||||||
if (/^(key|keyword|searchTerms)$/.test(value)) return encodeRuleParam(keyword || '');
|
if (/^(key|keyword|searchTerms)$/.test(value)) return encodeRuleParam(keyword || '');
|
||||||
if (/^(page|pageIndex)$/.test(value)) return String(page);
|
if (/^(page|pageIndex)$/.test(value)) return String(page);
|
||||||
const evaluated = safeEvalTemplateExpression(value, keyword, page);
|
const evaluated = safeEvalTemplateExpression(value, keyword, page);
|
||||||
|
// 表达式里如果已经显式调用 encodeURIComponent/encodeURI/java.encodeURI,
|
||||||
|
// 结果就是编码后的参数,不能再 encode 一次,否则会把 % 编成 %25。
|
||||||
|
if (/\b(?:encodeURIComponent|encodeURI|java\.encodeURI|java\.encodeURIComponent)\s*\(/.test(value)) return evaluated;
|
||||||
return encodeRuleParam(evaluated);
|
return encodeRuleParam(evaluated);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -211,6 +214,7 @@ function makeSourceRuntime(source: any, baseUrl?: string) {
|
|||||||
|
|
||||||
function runJsSnippetRaw(code: string, context: Record<string, any>, timeout = 1000): any {
|
function runJsSnippetRaw(code: string, context: Record<string, any>, timeout = 1000): any {
|
||||||
const sourceRuntime = makeSourceRuntime(context.source, context.baseUrl);
|
const sourceRuntime = makeSourceRuntime(context.source, context.baseUrl);
|
||||||
|
let lastPut: any = null;
|
||||||
const java = {
|
const java = {
|
||||||
base64Encode: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'),
|
base64Encode: (value: unknown) => Buffer.from(String(value ?? ''), 'utf8').toString('base64'),
|
||||||
base64Decode: (value: unknown) => Buffer.from(String(value ?? ''), 'base64').toString('utf8'),
|
base64Decode: (value: unknown) => Buffer.from(String(value ?? ''), 'base64').toString('utf8'),
|
||||||
@@ -223,9 +227,11 @@ function runJsSnippetRaw(code: string, context: Record<string, any>, timeout = 1
|
|||||||
deviceID: () => '',
|
deviceID: () => '',
|
||||||
androidId: () => '',
|
androidId: () => '',
|
||||||
longToast: () => undefined,
|
longToast: () => undefined,
|
||||||
|
toast: () => undefined,
|
||||||
|
startBrowserAwait: () => ({ body: () => '' }),
|
||||||
encodeURI: (value: unknown) => encodeURIComponent(String(value ?? '')),
|
encodeURI: (value: unknown) => encodeURIComponent(String(value ?? '')),
|
||||||
encodeURIComponent: (value: unknown) => encodeURIComponent(String(value ?? '')),
|
encodeURIComponent: (value: unknown) => encodeURIComponent(String(value ?? '')),
|
||||||
put: (key: string, value: any) => { variableStore.set(key, value); return value; },
|
put: (key: string, value: any) => { lastPut = { key, value }; variableStore.set(key, value); return value; },
|
||||||
get: (key: string) => variableStore.get(key),
|
get: (key: string) => variableStore.get(key),
|
||||||
log: () => undefined,
|
log: () => undefined,
|
||||||
htmlFormat: (value: unknown) => he.decode(String(value ?? '')).replace(/<br\s*\/?/gi, '\n').replace(/<[^>]+>/g, ''),
|
htmlFormat: (value: unknown) => he.decode(String(value ?? '')).replace(/<br\s*\/?/gi, '\n').replace(/<[^>]+>/g, ''),
|
||||||
@@ -260,14 +266,17 @@ function runJsSnippetRaw(code: string, context: Record<string, any>, timeout = 1
|
|||||||
const result = script.runInNewContext(sandbox, { timeout });
|
const result = script.runInNewContext(sandbox, { timeout });
|
||||||
if (result !== undefined && result !== null && result !== '') return result;
|
if (result !== undefined && result !== null && result !== '') return result;
|
||||||
if (sandbox.result !== context.result && sandbox.result !== undefined && sandbox.result !== null && sandbox.result !== '') return sandbox.result;
|
if (sandbox.result !== context.result && sandbox.result !== undefined && sandbox.result !== null && sandbox.result !== '') return sandbox.result;
|
||||||
|
if (lastPut?.key === 'url') return lastPut.value;
|
||||||
} catch {
|
} catch {
|
||||||
// 如果 @js 后面是单个表达式(例如 header: @js:JSON.stringify({...})),上面的函数体不会自动返回。
|
// 如果 @js 后面是单个表达式(例如 header: @js:JSON.stringify({...})),上面的函数体不会自动返回。
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const script = new vm.Script(`(function(){ ${body}
|
const script = new vm.Script(`(function(){ ${body}
|
||||||
; if (typeof result !== 'undefined') return result; if (typeof res !== 'undefined') return res; return ''; })()`);
|
; if (typeof res !== 'undefined') return res; return ''; })()`);
|
||||||
const result = script.runInNewContext(sandbox, { timeout });
|
const result = script.runInNewContext(sandbox, { timeout });
|
||||||
if (result !== undefined && result !== null && result !== '') return result;
|
if (result !== undefined && result !== null && result !== '') return result;
|
||||||
|
if (sandbox.result !== context.result && sandbox.result !== undefined && sandbox.result !== null && sandbox.result !== '') return sandbox.result;
|
||||||
|
if (lastPut?.key === 'url') return lastPut.value;
|
||||||
} catch {
|
} catch {
|
||||||
// fallback to expression mode below
|
// fallback to expression mode below
|
||||||
}
|
}
|
||||||
@@ -276,6 +285,7 @@ function runJsSnippetRaw(code: string, context: Record<string, any>, timeout = 1
|
|||||||
const result = script.runInNewContext(sandbox, { timeout });
|
const result = script.runInNewContext(sandbox, { timeout });
|
||||||
if (result !== undefined && result !== null && result !== '') return result;
|
if (result !== undefined && result !== null && result !== '') return result;
|
||||||
if (sandbox.result !== context.result && sandbox.result !== undefined && sandbox.result !== null && sandbox.result !== '') return sandbox.result;
|
if (sandbox.result !== context.result && sandbox.result !== undefined && sandbox.result !== null && sandbox.result !== '') return sandbox.result;
|
||||||
|
if (lastPut?.key === 'url') return lastPut.value;
|
||||||
return '';
|
return '';
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
@@ -593,7 +603,7 @@ function parseStep(step: string): { selector: string; attr: string } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stripFilters(rule: string) {
|
function stripFilters(rule: string) {
|
||||||
return splitRuleFilters(rule).base;
|
return splitRuleFilters(stripRuleJsBlocks(rule)).base;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyLegadoIndexSelector(current: cheerio.Cheerio<any>, selector: string): { current: cheerio.Cheerio<any>; selector: string } {
|
function applyLegadoIndexSelector(current: cheerio.Cheerio<any>, selector: string): { current: cheerio.Cheerio<any>; selector: string } {
|
||||||
@@ -606,7 +616,7 @@ function applyLegadoIndexSelector(current: cheerio.Cheerio<any>, selector: strin
|
|||||||
const real = idx < 0 ? current.length + idx : idx;
|
const real = idx < 0 ? current.length + idx : idx;
|
||||||
return { current: current.filter((index) => index !== real), selector: '' };
|
return { current: current.filter((index) => index !== real), selector: '' };
|
||||||
}
|
}
|
||||||
const range = normalized.match(/(?:\[(-?\d*):(-?\d*)(?::(-?\d+))?\]|\.(-?\d*):(-?\d*))$/);
|
const range = normalized.match(/(?:\[(-?\d*):(-?\d*)(?::(-?\d+))?\]|\.(-?\d*):(-?\d*)(?::(-?\d+))?)$/);
|
||||||
if (range) {
|
if (range) {
|
||||||
normalized = normalized.slice(0, range.index).trim();
|
normalized = normalized.slice(0, range.index).trim();
|
||||||
current = normalized ? current.find(normalized) : current;
|
current = normalized ? current.find(normalized) : current;
|
||||||
@@ -617,7 +627,9 @@ function applyLegadoIndexSelector(current: cheerio.Cheerio<any>, selector: strin
|
|||||||
const end = endRaw ? Number(endRaw) : length;
|
const end = endRaw ? Number(endRaw) : length;
|
||||||
const realStart = start < 0 ? length + start : start;
|
const realStart = start < 0 ? length + start : start;
|
||||||
const realEnd = end < 0 ? length + end : end;
|
const realEnd = end < 0 ? length + end : end;
|
||||||
return { current: current.slice(realStart, realEnd), selector: '' };
|
const step = Number(range[3] ?? range[6] ?? 1) || 1;
|
||||||
|
const sliced = current.slice(realStart, realEnd);
|
||||||
|
return { current: step === 1 ? sliced : sliced.filter((_, index) => index % Math.abs(step) === 0), selector: '' };
|
||||||
}
|
}
|
||||||
const indexMatch = normalized.match(/(?:\[(-?\d+)\]|\.(-?\d+))$/);
|
const indexMatch = normalized.match(/(?:\[(-?\d+)\]|\.(-?\d+))$/);
|
||||||
if (indexMatch) {
|
if (indexMatch) {
|
||||||
@@ -644,7 +656,7 @@ function applyLegadoSelector($: cheerio.CheerioAPI, current: cheerio.Cheerio<any
|
|||||||
if (
|
if (
|
||||||
tokens.length > 1
|
tokens.length > 1
|
||||||
&& tokens.every((token) => !/[>+~]/.test(token))
|
&& tokens.every((token) => !/[>+~]/.test(token))
|
||||||
&& tokens.some((token) => /(?:\[!?\-?\d+\]|\[-?\d*:|-?\d+\]$|\.-?\d+(?::\-?\d*)?)$/.test(token))
|
&& tokens.some((token) => /(?:\[!?\-?\d+\]|\[-?\d*:|-?\d+\]$|\.-?\d+(?::\-?\d*){0,2})$/.test(token))
|
||||||
) {
|
) {
|
||||||
let next = current;
|
let next = current;
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
@@ -728,6 +740,22 @@ function selectXPath($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule: st
|
|||||||
|
|
||||||
function readValue($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: string, baseUrl?: string, jsContext?: Record<string, any>): string {
|
function readValue($: cheerio.CheerioAPI, root: cheerio.Cheerio<any>, rule?: string, baseUrl?: string, jsContext?: Record<string, any>): string {
|
||||||
for (const alternative of splitAlternatives(rule)) {
|
for (const alternative of splitAlternatives(rule)) {
|
||||||
|
if (alternative.includes('{{baseUrl}}')) {
|
||||||
|
const { base, filters } = splitRuleFilters(alternative);
|
||||||
|
const value = applyRuleFilters(base.replace(/\{\{baseUrl\}\}/g, baseUrl || ''), filters);
|
||||||
|
if (value) return value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^<js>/i.test(alternative.trim())) {
|
||||||
|
const transformed = runJsSnippet(alternative, { ...(jsContext || {}), result: root.text(), src: root.text(), baseUrl });
|
||||||
|
if (transformed) return /^(?:https?:)?\/\//i.test(transformed) || transformed.startsWith('/') ? normalizeUrl(baseUrl || '', transformed) : transformed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/^@js:/i.test(alternative.trim())) {
|
||||||
|
const transformed = runJsSnippet(alternative, { ...(jsContext || {}), result: root.text(), src: root.text(), baseUrl });
|
||||||
|
if (transformed) return /^(?:https?:)?\/\//i.test(transformed) || transformed.startsWith('/') ? normalizeUrl(baseUrl || '', transformed) : transformed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const jsIndex = alternative.indexOf('@js:');
|
const jsIndex = alternative.indexOf('@js:');
|
||||||
if (jsIndex > 0) {
|
if (jsIndex > 0) {
|
||||||
const selectorRule = alternative.slice(0, jsIndex).trim();
|
const selectorRule = alternative.slice(0, jsIndex).trim();
|
||||||
@@ -1173,7 +1201,7 @@ function parseExploreUrl(exploreUrl?: string): Array<{ title: string; template:
|
|||||||
.filter((item) => !!item.title);
|
.filter((item) => !!item.title);
|
||||||
}
|
}
|
||||||
return raw
|
return raw
|
||||||
.split('&&')
|
.split(/&&|\r?\n+/)
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
@@ -1582,7 +1610,12 @@ export class LegadoClient {
|
|||||||
const normalizedNext = normalizeUrl(targetUrl, nextTocUrl);
|
const normalizedNext = normalizeUrl(targetUrl, nextTocUrl);
|
||||||
if (!normalizedNext || visited.has(normalizedNext)) break;
|
if (!normalizedNext || visited.has(normalizedNext)) break;
|
||||||
visited.add(normalizedNext);
|
visited.add(normalizedNext);
|
||||||
const nextHtml = await fetchText(source, normalizedNext);
|
let nextHtml = '';
|
||||||
|
try {
|
||||||
|
nextHtml = await fetchText(source, normalizedNext);
|
||||||
|
} catch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
const $next = cheerio.load(nextHtml);
|
const $next = cheerio.load(nextHtml);
|
||||||
const nextItems = selectElements($next, $next.root(), rule.ruleToc.chapterList);
|
const nextItems = selectElements($next, $next.root(), rule.ruleToc.chapterList);
|
||||||
nextItems.each((index, element) => {
|
nextItems.each((index, element) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user