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); });