原方式在 landingPage body 全文搜索版本字符串,不可靠。 改为导航到 wp-admin/plugins.php 从插件列表行精确提取, 并自动从蓝图 zip URL 获取预期版本号、附带激活状态检测。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
274 lines
10 KiB
JavaScript
Executable file
274 lines
10 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
||
// play-verify — play.wenpai.net 线上插件快捷方式验证工具
|
||
// 用法: play-verify <plugin-id> [--all] [--screenshot] [--report] [--post-forum]
|
||
// 示例: play-verify wpslug
|
||
// play-verify --all
|
||
// play-verify wpslug --screenshot
|
||
// play-verify --all --report
|
||
// play-verify --all --report --post-forum
|
||
|
||
const { chromium } = require('playwright');
|
||
const { execSync } = require('child_process');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const SITE = 'https://play.wenpai.net';
|
||
const PLUGINS_JSON = path.join(process.env.HOME, 'play/plugins.json');
|
||
const SCREENSHOT_DIR = path.join(process.env.HOME, 'test-results', new Date().toISOString().slice(0, 10), 'play-verify');
|
||
|
||
const args = process.argv.slice(2);
|
||
const doAll = args.includes('--all');
|
||
const doScreenshot = args.includes('--screenshot');
|
||
const doReport = args.includes('--report');
|
||
const doForum = args.includes('--post-forum');
|
||
const pluginId = args.find(a => !a.startsWith('--'));
|
||
|
||
if (!pluginId && !doAll) {
|
||
console.log('用法: play-verify <plugin-id> [--all] [--screenshot] [--report] [--post-forum]');
|
||
console.log(' play-verify wpslug 验证单个插件');
|
||
console.log(' play-verify --all 验证所有插件');
|
||
console.log(' play-verify wpslug --screenshot 验证并截图');
|
||
console.log(' play-verify --all --report 验证并生成 markdown 报告');
|
||
console.log(' play-verify --all --report --post-forum 报告并发论坛');
|
||
process.exit(0);
|
||
}
|
||
|
||
async function getPlugins() {
|
||
const data = JSON.parse(fs.readFileSync(PLUGINS_JSON, 'utf8'));
|
||
// 排除非插件项(featured/env 类型没有 ?plugin= 快捷方式)
|
||
return data.filter(p => p.group === 'plugin');
|
||
}
|
||
|
||
function extractVersionFromBlueprint(id) {
|
||
const bpPath = path.join(process.env.HOME, 'play/blueprints', `${id}.json`);
|
||
try {
|
||
const bp = JSON.parse(fs.readFileSync(bpPath, 'utf8'));
|
||
for (const step of bp.steps || []) {
|
||
if (step.step === 'installPlugin' && step.pluginData?.url) {
|
||
const m = step.pluginData.url.match(/[\w-]-([\d.]+)\.zip$/);
|
||
if (m) return m[1];
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
return null;
|
||
}
|
||
|
||
async function verifyPlugin(browser, plugin) {
|
||
const id = plugin.id || plugin;
|
||
const name = plugin.name || id;
|
||
const results = { id, name, checks: [], pass: true };
|
||
// 自动从蓝图提取版本号(plugins.json 没有时)
|
||
if (!plugin.version) plugin.version = extractVersionFromBlueprint(id);
|
||
|
||
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
||
|
||
try {
|
||
// 1. 快捷方式 302 重定向
|
||
const resp = await page.goto(`${SITE}/?plugin=${id}`, { waitUntil: 'load', timeout: 60000 });
|
||
const chain = resp.request().redirectedFrom();
|
||
if (chain) {
|
||
const location = page.url();
|
||
const hasFullUrl = location.includes('blueprint-url=https');
|
||
results.checks.push({ test: '302 重定向', pass: true, detail: hasFullUrl ? '完整 URL' : '相对路径' });
|
||
if (!hasFullUrl) results.pass = false;
|
||
} else {
|
||
results.checks.push({ test: '302 重定向', pass: false, detail: '无重定向' });
|
||
results.pass = false;
|
||
}
|
||
|
||
// 2. 等待 Playground 加载
|
||
await page.waitForTimeout(35000);
|
||
|
||
// 3. 检查蓝图加载错误
|
||
const outerBody = await page.textContent('body').catch(() => '');
|
||
const blueprintError = outerBody.includes('蓝图加载失败') || outerBody.includes('Blueprint file');
|
||
results.checks.push({ test: '蓝图加载', pass: !blueprintError, detail: blueprintError ? '加载失败' : 'OK' });
|
||
if (blueprintError) {
|
||
results.pass = false;
|
||
await page.close();
|
||
return results;
|
||
}
|
||
|
||
// 4. 找到 WordPress iframe
|
||
let wpFrame = null;
|
||
for (const frame of page.frames()) {
|
||
const url = frame.url();
|
||
if (url.includes('scope:') || (url.includes('wp-admin') && !url.includes('playground.html'))) {
|
||
wpFrame = frame;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!wpFrame) {
|
||
results.checks.push({ test: 'WordPress iframe', pass: false, detail: '未找到' });
|
||
results.pass = false;
|
||
await page.close();
|
||
return results;
|
||
}
|
||
|
||
results.checks.push({ test: 'WordPress iframe', pass: true, detail: wpFrame.url().substring(0, 80) });
|
||
|
||
// 5. iframe 内容检查
|
||
const wpBody = await wpFrame.textContent('body').catch(() => '');
|
||
|
||
// PHP 错误
|
||
const phpError = wpBody.includes('Fatal error') || wpBody.includes('Warning:') || wpBody.includes('Parse error');
|
||
results.checks.push({ test: '无 PHP 错误', pass: !phpError, detail: phpError ? '有错误' : 'OK' });
|
||
if (phpError) results.pass = false;
|
||
|
||
// 版本号 — 从 plugins.php 插件列表提取(最可靠来源)
|
||
const expectedVersion = plugin.version ? plugin.version.replace('v', '') : null;
|
||
if (expectedVersion) {
|
||
let versionFound = false;
|
||
let versionDetail = '未检测到';
|
||
try {
|
||
await wpFrame.evaluate(() => { window.location.href = '/wp-admin/plugins.php'; });
|
||
await wpFrame.waitForSelector('.wp-list-table', { timeout: 15000 });
|
||
const pluginRow = await wpFrame.evaluateHandle((pluginId) => {
|
||
for (const row of document.querySelectorAll('tr[data-slug]')) {
|
||
const cb = row.querySelector('input[type="checkbox"]');
|
||
if (cb && cb.value.startsWith(pluginId + '/')) return row;
|
||
}
|
||
return null;
|
||
}, id);
|
||
const isValid = await pluginRow.evaluate(el => el instanceof HTMLElement).catch(() => false);
|
||
if (isValid) {
|
||
const versionText = await pluginRow.evaluate(el => {
|
||
const ve = el.querySelector('.plugin-version-author-uri');
|
||
return ve ? ve.textContent : '';
|
||
});
|
||
const match = versionText.match(/([\d.]+)\s*版本/i) || versionText.match(/(?:Version)\s*([\d.]+)/i);
|
||
if (match) {
|
||
versionFound = match[1] === expectedVersion;
|
||
versionDetail = `plugins.php: ${match[1]}`;
|
||
} else {
|
||
versionDetail = 'plugins.php: 版本文本未匹配';
|
||
}
|
||
// 附赠:检查插件激活状态
|
||
const isActive = await pluginRow.evaluate(el => el.classList.contains('active'));
|
||
results.checks.push({ test: '插件激活', pass: isActive, detail: isActive ? '已激活' : '未激活' });
|
||
if (!isActive) results.pass = false;
|
||
} else {
|
||
versionDetail = 'plugins.php: 未找到插件行';
|
||
}
|
||
} catch (_) {
|
||
// Fallback: 回退到 body 全文搜索
|
||
const wpBodyFallback = await wpFrame.textContent('body').catch(() => '');
|
||
versionFound = wpBodyFallback.includes(expectedVersion);
|
||
versionDetail = versionFound ? `fallback: ${expectedVersion}` : 'fallback: 未检测到';
|
||
}
|
||
results.checks.push({ test: `版本号 ${expectedVersion}`, pass: versionFound, detail: versionDetail });
|
||
if (!versionFound) results.pass = false;
|
||
}
|
||
|
||
// 中文界面
|
||
const isChinese = wpBody.includes('设置') || wpBody.includes('仪表盘') || wpBody.includes('插件') || wpBody.includes('文章');
|
||
results.checks.push({ test: '中文界面', pass: isChinese, detail: isChinese ? 'OK' : '未检测到中文' });
|
||
|
||
// 截图
|
||
if (doScreenshot) {
|
||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||
await page.screenshot({ path: path.join(SCREENSHOT_DIR, `${id}.png`), fullPage: true });
|
||
results.checks.push({ test: '截图', pass: true, detail: `${SCREENSHOT_DIR}/${id}.png` });
|
||
}
|
||
|
||
} catch (err) {
|
||
results.checks.push({ test: '运行异常', pass: false, detail: err.message.substring(0, 100) });
|
||
results.pass = false;
|
||
} finally {
|
||
await page.close();
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
function generateReport(allResults) {
|
||
const now = new Date();
|
||
const dateStr = now.toISOString().slice(0, 10);
|
||
const timeStr = now.toTimeString().slice(0, 5);
|
||
const passed = allResults.filter(r => r.pass).length;
|
||
const failed = allResults.filter(r => !r.pass).length;
|
||
const total = allResults.length;
|
||
|
||
let md = `# play.wenpai.net 验证报告\n\n`;
|
||
md += `**日期**: ${dateStr} ${timeStr} \n`;
|
||
md += `**总览**: ${passed} 通过 / ${failed} 失败 / ${total} 总计\n\n`;
|
||
|
||
md += `## 逐插件检查\n\n`;
|
||
for (const r of allResults) {
|
||
const icon = r.pass ? '✅' : '❌';
|
||
md += `### ${icon} ${r.name} (\`${r.id}\`)\n\n`;
|
||
for (const c of r.checks) {
|
||
md += `- [${c.pass ? 'x' : ' '}] **${c.test}**: ${c.detail}\n`;
|
||
}
|
||
md += `\n`;
|
||
}
|
||
|
||
if (failed > 0) {
|
||
md += `## 失败汇总\n\n`;
|
||
for (const r of allResults.filter(r => !r.pass)) {
|
||
const failedChecks = r.checks.filter(c => !c.pass).map(c => c.test).join(', ');
|
||
md += `- **${r.name}**: ${failedChecks}\n`;
|
||
}
|
||
md += `\n`;
|
||
}
|
||
|
||
return md;
|
||
}
|
||
|
||
(async () => {
|
||
const browser = await chromium.launch({ headless: true });
|
||
const plugins = doAll ? await getPlugins() : [pluginId];
|
||
const allResults = [];
|
||
|
||
for (const p of plugins) {
|
||
const plugin = typeof p === 'string' ? { id: p, name: p } : p;
|
||
process.stdout.write(`\n验证 ${plugin.name || plugin.id} ... `);
|
||
|
||
const result = await verifyPlugin(browser, plugin);
|
||
allResults.push(result);
|
||
|
||
console.log(result.pass ? 'PASS' : 'FAIL');
|
||
for (const c of result.checks) {
|
||
console.log(` [${c.pass ? '✓' : '✗'}] ${c.test}: ${c.detail}`);
|
||
}
|
||
}
|
||
|
||
await browser.close();
|
||
|
||
// 汇总
|
||
const passed = allResults.filter(r => r.pass).length;
|
||
const failed = allResults.filter(r => !r.pass).length;
|
||
console.log(`\n=== 汇总: ${passed} 通过, ${failed} 失败, 共 ${allResults.length} ===`);
|
||
|
||
if (doScreenshot) {
|
||
console.log(`截图目录: ${SCREENSHOT_DIR}`);
|
||
}
|
||
|
||
// 保存结果
|
||
if (doAll || doScreenshot || doReport || doForum) {
|
||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||
fs.writeFileSync(path.join(SCREENSHOT_DIR, 'results.json'), JSON.stringify(allResults, null, 2));
|
||
}
|
||
|
||
// 生成 markdown 报告
|
||
if (doReport || doForum) {
|
||
const report = generateReport(allResults);
|
||
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
||
const reportPath = path.join(SCREENSHOT_DIR, 'verify-report.md');
|
||
fs.writeFileSync(reportPath, report);
|
||
console.log(`报告: ${reportPath}`);
|
||
|
||
if (doForum) {
|
||
try {
|
||
const title = `play.wenpai.net 验证报告 ${new Date().toISOString().slice(0, 10)}`;
|
||
execSync(`forum-cli post --title "${title}" --body "${report.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
|
||
console.log('已发布到论坛');
|
||
} catch (e) {
|
||
console.error('论坛发布失败:', e.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
process.exit(failed > 0 ? 1 : 0);
|
||
})();
|