181 lines
5.4 KiB
JavaScript
181 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import readline from 'node:readline/promises';
|
|
import { stdin as input, stdout as output } from 'node:process';
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
headless: false,
|
|
manualLogin: false,
|
|
save: '',
|
|
};
|
|
const positional = [];
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const token = argv[index];
|
|
if (token === '--headless') {
|
|
args.headless = true;
|
|
continue;
|
|
}
|
|
if (token === '--manual-login') {
|
|
args.manualLogin = true;
|
|
continue;
|
|
}
|
|
if (token === '--save') {
|
|
args.save = argv[index + 1] ?? '';
|
|
index += 1;
|
|
continue;
|
|
}
|
|
positional.push(token);
|
|
}
|
|
|
|
args.url = positional[0];
|
|
return args;
|
|
}
|
|
|
|
async function prompt(message) {
|
|
const rl = readline.createInterface({ input, output });
|
|
const answer = await rl.question(message);
|
|
rl.close();
|
|
return answer.trim();
|
|
}
|
|
|
|
async function importPlaywright() {
|
|
try {
|
|
return await import('playwright');
|
|
} catch (error) {
|
|
console.error('未检测到 playwright 依赖。请先安装: npm i playwright');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function collectSelectors(page, candidates) {
|
|
const result = {};
|
|
for (const [name, selector] of Object.entries(candidates)) {
|
|
const locator = page.locator(selector);
|
|
const count = await locator.count();
|
|
result[name] = {
|
|
selector,
|
|
count,
|
|
};
|
|
if (count > 0) {
|
|
result[name].sampleText = (await locator.first().innerText().catch(() => '')).slice(0, 120);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function analyze(url, options) {
|
|
const { chromium } = await importPlaywright();
|
|
const browser = await chromium.launch({ headless: options.headless });
|
|
const context = await browser.newContext({
|
|
viewport: { width: 1440, height: 900 },
|
|
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122 Safari/537.36',
|
|
});
|
|
const page = await context.newPage();
|
|
const requests = [];
|
|
|
|
page.on('response', (response) => {
|
|
const resourceType = response.request().resourceType();
|
|
if (['xhr', 'fetch', 'document'].includes(resourceType)) {
|
|
requests.push({
|
|
status: response.status(),
|
|
method: response.request().method(),
|
|
resourceType,
|
|
url: response.url(),
|
|
});
|
|
}
|
|
});
|
|
|
|
const notes = [];
|
|
|
|
try {
|
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
|
|
const loginIndicators = await collectSelectors(page, {
|
|
loginButton: 'a[href*="login"], button:has-text("登录"), button:has-text("Sign in"), .login, .signin',
|
|
passwordInput: 'input[type="password"]',
|
|
searchInput: 'input[type="search"], input[name*="search"], input[name*="keyword"], input[placeholder*="搜索"]',
|
|
bookLinks: 'a[href*="book"], a[href*="novel"], .book-link, .novel-link',
|
|
chapterLinks: 'a[href*="chapter"], a[href*="read"], .chapter a',
|
|
contentContainer: '#content, .content, .read-content, .chapter-content',
|
|
});
|
|
|
|
const title = await page.title();
|
|
const currentUrl = page.url();
|
|
|
|
if (loginIndicators.passwordInput.count > 0 || loginIndicators.loginButton.count > 0) {
|
|
notes.push('检测到登录相关元素,建议优先确认是否需要登录后再分析。');
|
|
}
|
|
|
|
if (options.manualLogin) {
|
|
notes.push('已启用人工登录流程。');
|
|
console.log('浏览器已打开。若网站需要登录,请由人类在浏览器中完成登录。');
|
|
await prompt('登录完成后按 Enter 继续分析。');
|
|
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
|
|
}
|
|
|
|
const finalSignals = await collectSelectors(page, {
|
|
searchInput: 'input[type="search"], input[name*="search"], input[name*="keyword"], input[placeholder*="搜索"]',
|
|
resultItems: '.book-item, .novel-item, .book-list li, .search-result li',
|
|
tocItems: '.chapter-list li, .directory li, .volume-list li',
|
|
contentContainer: '#content, .content, .read-content, .chapter-content',
|
|
});
|
|
|
|
const report = {
|
|
url,
|
|
initialTitle: title,
|
|
finalUrl: currentUrl,
|
|
notes,
|
|
loginIndicators,
|
|
finalSignals,
|
|
capturedRequests: requests.slice(0, 30),
|
|
};
|
|
|
|
return { report, browser };
|
|
} catch (error) {
|
|
await browser.close();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
if (!args.url) {
|
|
console.error('用法: node scripts/analyze_with_playwright.mjs <网站URL> [--manual-login] [--save analysis.json] [--headless]');
|
|
process.exit(1);
|
|
}
|
|
|
|
const normalizedUrl = args.url.startsWith('http') ? args.url : `https://${args.url}`;
|
|
|
|
try {
|
|
const { report, browser } = await analyze(normalizedUrl, args);
|
|
console.log(JSON.stringify(report, null, 2));
|
|
|
|
if (args.save) {
|
|
const savePath = path.resolve(args.save);
|
|
await fs.writeFile(savePath, JSON.stringify(report, null, 2), 'utf8');
|
|
console.log(`分析结果已保存到: ${savePath}`);
|
|
}
|
|
|
|
if (!args.headless) {
|
|
const answer = await prompt('按 Enter 关闭浏览器,或输入 keep 保持浏览器打开: ');
|
|
if (answer.toLowerCase() !== 'keep') {
|
|
await browser.close();
|
|
}
|
|
return;
|
|
}
|
|
|
|
await browser.close();
|
|
} catch (error) {
|
|
console.error(`分析失败: ${error.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
await main();
|