import { spawn, type ChildProcess } from 'node:child_process' import { existsSync, mkdirSync, writeFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { createServer, type ViteDevServer } from 'vite' import { ensurePacksAvailable } from './verify-ground-combat-browser.ts' const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)) const CHROME_CANDIDATES = [ '/usr/bin/google-chrome', '/usr/local/google/home/taodao/.cache/ms-playwright/chromium-1228/chrome-linux64/chrome', '/root/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome', ] function resolveChromeBinary(): string { for (const bin of CHROME_CANDIDATES) { if (existsSync(bin)) return bin } throw new Error('No accessible Chromium binary found') } async function main() { const outputDir = process.env.VENDOR_AUDIT_OUT ?? '/usr/local/google/home/taodao/.gemini/jetski/brain/ba5784fe-f165-4cff-aa7e-c3cfdf22c420' mkdirSync(outputDir, { recursive: true }) ensurePacksAvailable() const port = 5419 const viteServer: ViteDevServer = await createServer({ root: resolve('.'), logLevel: 'silent', server: { host: '127.0.0.1', port, strictPort: false }, }) await viteServer.listen() const actualPort = (viteServer.httpServer?.address() as any)?.port ?? port const targetUrl = `http://127.0.0.1:${actualPort}/acts.html?act=1&level=1-act-1-town-1` const chromeBin = resolveChromeBinary() const debugPort = 9519 const chromeProc: ChildProcess = spawn( chromeBin, [ '--headless=new', '--no-sandbox', '--disable-dev-shm-usage', '--window-size=1280,840', '--enable-webgl', '--ignore-gpu-blocklist', '--use-gl=angle', '--use-angle=swiftshader', '--no-proxy-server', `--remote-debugging-port=${debugPort}`, targetUrl, ], { stdio: ['ignore', 'ignore', 'pipe'] }, ) let ws: WebSocket | null = null try { let wsDebuggerUrl: string | null = null for (let i = 0; i < 80; i++) { await sleep(150) try { const res = await fetch(`http://127.0.0.1:${debugPort}/json`) const pages = (await res.json()) as any[] const page = pages.find(p => p.type === 'page' && p.url.includes('acts.html')) if (page?.webSocketDebuggerUrl) { wsDebuggerUrl = page.webSocketDebuggerUrl break } } catch {} } if (!wsDebuggerUrl) throw new Error('Failed to attach to Chrome CDP') ws = new WebSocket(wsDebuggerUrl) await new Promise((res, rej) => { ws!.onopen = () => res(true) ws!.onerror = rej }) let cmdId = 1 const pending = new Map void>() ws.onmessage = (event: MessageEvent) => { const msg = JSON.parse(String(event.data)) if (msg.id && pending.has(msg.id)) { pending.get(msg.id)!(msg) pending.delete(msg.id) } } const cdp = (method: string, params: Record = {}): Promise => new Promise((resolve, reject) => { const id = cmdId++ pending.set(id, (msg: any) => { if (msg.error) reject(new Error(JSON.stringify(msg.error))) else resolve(msg.result as T) }) ws!.send(JSON.stringify({ id, method, params })) }) const evalJs = async (expression: string): Promise => { const res = await cdp<{ result: { value: T }; exceptionDetails?: any }>('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true, }) if (res.exceptionDetails) { throw new Error(`Eval error: ${JSON.stringify(res.exceptionDetails)}`) } return res.result.value } const takeScreenshot = async (name: string): Promise => { const shot = await cdp<{ data: string }>('Page.captureScreenshot', { format: 'png' }) const filePath = join(outputDir, `${name}.png`) writeFileSync(filePath, Buffer.from(shot.data, 'base64')) console.log(`Saved screenshot: ${filePath}`) return filePath } await cdp('Runtime.enable') await cdp('Page.enable') for (let i = 0; i < 100; i++) { const ready = await evalJs(`Boolean(window.__d2webAct?.ready && window.__d2webHud?.ready)`) if (ready) break await sleep(200) } // 1. Open Charsi NPC Menu const menuInfo = await evalJs(`(() => { const hud = window.__d2webHudInstance; hud.openNpcMenuByName('Charsi', 400, 220); hud.render(); return { npcName: hud.worldPanels.npcMenu?.npcName, options: hud.worldPanels.npcMenu?.options?.map(o => o.labelZh), }; })()`) console.log('NPC Menu:', JSON.stringify(menuInfo)) await sleep(250) await takeScreenshot('vendor_npc_menu_charsi') // 2. Open Charsi Trade/Repair 4-tab 10x10 Vendor UI const charsiInfo = await evalJs(`(() => { const hud = window.__d2webHudInstance; hud.charSheet.attrs.level = 24; hud.openVendorSession('Charsi', 'trade', 0); const wp = hud.worldPanels; wp.activeVendorTab = 'armor'; const armorItems = wp.vendorTabPlacements.armor; const docking = hud.getDockingLayout(); const deltaLeft = -docking.marginW; if (armorItems.length > 0) { const first = armorItems[0]; wp.hoveredVendorItem = { item: first.item, x: 80 + 17 + first.col * 29 + 14, y: 60 + 64 + first.row * 29 + 14, col: first.col, row: first.row, }; hud.inventory.hoveredItem = { item: first.item, x: wp.hoveredVendorItem.x + deltaLeft, y: wp.hoveredVendorItem.y, }; } hud.render(); return { leftPanel: hud.leftPanel, rightPanel: hud.rightPanel, armorCount: wp.vendorTabPlacements.armor.length, weapons1Count: wp.vendorTabPlacements.weapons1.length, miscCount: wp.vendorTabPlacements.misc.length, firstItem: armorItems[0] ? { nameZh: armorItems[0].item.nameZh, buyCost: armorItems[0].item.buyCost } : null, }; })()`) console.log('Charsi Trade UI:', JSON.stringify(charsiInfo)) await sleep(250) await takeScreenshot('vendor_trade_charsi') // 3. Open Gheed Gamble UI const gheedInfo = await evalJs(`(() => { const hud = window.__d2webHudInstance; hud.charSheet.attrs.level = 60; hud.openVendorSession('Gheed', 'gamble', 1); const wp = hud.worldPanels; const gambleItems = wp.gamblePlacements; const docking = hud.getDockingLayout(); const deltaLeft = -docking.marginW; if (gambleItems.length > 0) { const first = gambleItems[0]; wp.hoveredVendorItem = { item: first.item, x: 80 + 17 + first.col * 29 + 14, y: 60 + 64 + first.row * 29 + 14, col: first.col, row: first.row, }; hud.inventory.hoveredItem = { item: first.item, x: wp.hoveredVendorItem.x + deltaLeft, y: wp.hoveredVendorItem.y, }; } hud.render(); return { gambleCount: gambleItems.length, firstGamble: gambleItems[0] ? { nameZh: gambleItems[0].item.nameZh, buyCost: gambleItems[0].item.buyCost } : null, }; })()`) console.log('Gheed Gamble UI:', JSON.stringify(gheedInfo)) await sleep(250) await takeScreenshot('vendor_gamble_gheed') } finally { ws?.close() chromeProc.kill('SIGKILL') await viteServer.close() } } main().catch(err => { console.error(err) process.exit(1) })