play.wenpai.net/scripts/play-verify
elementary-qa 8bf20426c4 feat: play-verify 版本号检测改为从 plugins.php 提取
原方式在 landingPage body 全文搜索版本字符串,不可靠。
改为导航到 wp-admin/plugins.php 从插件列表行精确提取,
并自动从蓝图 zip URL 获取预期版本号、附带激活状态检测。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:01:55 +08:00

274 lines
10 KiB
JavaScript
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
})();