diablo2-web/scripts/audit-boss-kill-10-zh.js

184 lines
6.5 KiB
JavaScript

import { spawn } from 'child_process'
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function runBrowserAudit() {
console.log('1. Starting Google Chrome headless...')
const chromeProc = spawn('/usr/bin/google-chrome', [
'--headless=new',
'--remote-debugging-port=9222',
'--no-sandbox',
'--disable-gpu',
'--disable-extensions',
'http://localhost:8080/boss.html',
])
// Wait for Chrome to be ready
let wsUrl = null
for (let i = 0; i < 30; i++) {
await sleep(300)
try {
const res = await fetch('http://127.0.0.1:9222/json/list')
const pages = await res.json()
const page = pages.find(p => p.type === 'page' && p.url.includes('boss.html'))
if (page && page.webSocketDebuggerUrl) {
wsUrl = page.webSocketDebuggerUrl
break
}
} catch (e) {
// keep trying
}
}
if (!wsUrl) {
chromeProc.kill()
throw new Error('Failed to connect to Chrome headless CDP within 10s')
}
console.log('2. Connected to Chrome CDP on page:', wsUrl)
const ws = new WebSocket(wsUrl)
await new Promise(resolve => ws.onopen = resolve)
let idCounter = 1
function sendCommand(method, params = {}) {
return new Promise((resolve, reject) => {
const id = idCounter++
const handler = event => {
const msg = JSON.parse(event.data)
if (msg.id === id) {
ws.removeEventListener('message', handler)
if (msg.error) reject(msg.error)
else resolve(msg.result)
}
}
ws.addEventListener('message', handler)
ws.send(JSON.stringify({ id, method, params }))
})
}
// Enable Runtime
await sendCommand('Runtime.enable')
// Wait 1.5s for initial drop tables to load
await sleep(1500)
console.log('3. Triggering Chinese language switch and 10x Boss Kill in browser DOM...')
const evalResult = await sendCommand('Runtime.evaluate', {
expression: `(async () => {
// 1. Click Chinese language pill
const zhPill = document.querySelector('.lang-pill[data-lang="zh"]');
if (zhPill) zhPill.click();
// 2. Click 10x kill button
const kill10Btn = document.getElementById('btn-kill-10');
if (kill10Btn) kill10Btn.click();
// Wait a short moment for DOM rendering
await new Promise(r => setTimeout(r, 800));
// 3. Extract items
const cards = Array.from(document.querySelectorAll('#loot-items-grid .d2-item-card'));
return cards.map(c => {
const title = c.querySelector('.item-title')?.textContent?.trim() || '';
const subTitle = c.querySelector('.item-subtitle')?.textContent?.trim() || '';
const quality = c.querySelector('.item-quality-pill')?.textContent?.trim() || '';
const ilvl = c.querySelector('.item-ilvl-pill')?.textContent?.trim() || '';
const stats = Array.from(c.querySelectorAll('.item-base-stats .base-stat-item')).map(s => s.textContent.trim());
const affixes = Array.from(c.querySelectorAll('.item-affix-lines .tooltip-stat-line')).map(a => a.textContent.trim());
return {
title,
subTitle,
quality,
ilvl,
stats,
affixes
};
});
})()`,
awaitPromise: true,
returnByValue: true,
})
console.log('4. Evaluation completed. Closing Chrome...')
ws.close()
chromeProc.kill()
const items = evalResult.result.value
console.log(`\nSuccessfully captured ${items.length} items from http://localhost:8080/boss.html!\n`)
return items
}
runBrowserAudit()
.then(items => {
console.log('================================================================');
console.log(`=== AUDITING ALL ${items.length} ITEMS GENERATED FROM 10x BOSS KILL ===`);
console.log('================================================================\n');
let errorsFound = 0;
const traditionalChars = ['屬', '無', '紅', '綠', '變', '爛', '誕', '誠', '雲', '犧', '嚇', '層', '靜', '換', '亂', '節', '甦'];
items.forEach((item, index) => {
console.log(`[#${index + 1}] [${item.quality}] ${item.title} (底材: ${item.subTitle || '无'}, ${item.ilvl})`);
if (item.stats.length > 0) {
console.log(` 基础属性: ${item.stats.join(' | ')}`);
}
if (item.affixes.length > 0) {
console.log(` 词条属性:`);
item.affixes.forEach(aff => console.log(` * ${aff}`));
}
// Check 1: Negative numbers in charged spells
item.affixes.forEach(aff => {
if (aff.includes('次充能') && (aff.includes('等级 -') || aff.includes('(-'))) {
console.error(` ❌ ERROR: Negative value in charged spell: "${aff}"`);
errorsFound++;
}
});
// Check 2: Missing English affix translations (e.g. English letters followed by 之 or preceded by 之)
const englishAffixMatch = item.title.match(/[a-zA-Z]+之|之[a-zA-Z]+/);
if (englishAffixMatch) {
console.error(` ❌ ERROR: Untranslated English affix in title: "${item.title}"`);
errorsFound++;
}
// Check 3: Word order anomaly: base name followed by '之' at the end of title
if (item.subTitle && item.title.endsWith(item.subTitle + '之')) {
console.error(` ❌ ERROR: Word order inversion (base followed by suffix particle): "${item.title}"`);
errorsFound++;
}
// Check 4: Traditional Chinese characters
const allText = item.title + ' ' + item.subTitle + ' ' + item.affixes.join(' ');
traditionalChars.forEach(ch => {
if (allText.includes(ch)) {
console.error(` ❌ ERROR: Traditional Chinese character '${ch}' found in: "${allText}"`);
errorsFound++;
}
});
console.log('');
});
console.log('================================================================');
if (errorsFound === 0) {
console.log(`🎉 AUDIT PASSED! All ${items.length} items checked with ZERO errors!`);
console.log(`✅ All names in correct Chinese grammar word order`);
console.log(`✅ 100% Affix translations matched without fallback artifacts`);
console.log(`✅ Charged spell positive charges and positive skill levels verified`);
console.log(`✅ Normalized Simplified Chinese characters verified`);
} else {
console.error(`⚠️ AUDIT FOUND ${errorsFound} ERRORS!`);
process.exit(1);
}
console.log('================================================================\n');
})
.catch(err => {
console.error('Audit execution failed:', err);
process.exit(1);
});