#!/usr/bin/env node /** * 布局诊断 v4 — 针对 ChatInput + IoTStatusBar 不可见问题 */ import { writeFileSync, mkdirSync } from 'fs'; import WebSocket from 'ws'; const BASE = 'http://localhost:5199'; const API = 'http://localhost:8080/api/v1'; const CDP_PORT = parseInt(process.env.CDP_PORT || '9225'); const OUT = '/home/aska/Code/Cyrene/debug/logs/chromium'; mkdirSync(OUT, { recursive: true }); function makeCDPHelper(ws) { let msgId = 1; const pending = new Map(); ws.on('message', (raw) => { try { const msg = JSON.parse(raw.toString()); if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg.result || msg); pending.delete(msg.id); } } catch {} }); return (method, params = {}) => new Promise((resolve, reject) => { const id = msgId++; const timer = setTimeout(() => { pending.delete(id); reject(new Error(`CDP timeout: ${method}`)); }, 15000); ws.send(JSON.stringify({ id, method, params })); pending.set(id, (r) => { clearTimeout(timer); resolve(r); }); }); } async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function evaluate(cdp, expression, label) { try { const r = await cdp('Runtime.evaluate', { returnByValue: true, expression, }); const val = r?.result?.value ?? r?.value; if (label) console.log(` [${label}]:`, typeof val === 'string' ? val.slice(0, 500) : JSON.stringify(val).slice(0, 500)); return val; } catch (e) { console.error(` [${label}] ERROR:`, e.message); return null; } } async function main() { // Step 1: Login console.log('[1] Logging in...'); const loginRes = await fetch(`${API}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'admin123' }), }); const loginData = await loginRes.json(); const token = loginData?.token; const userId = loginData?.user_id || 'admin'; console.log(' userId:', userId, 'token:', token?.slice(0, 20) + '...'); if (!token) throw new Error('Login failed: ' + JSON.stringify(loginData)); // Step 2: Connect to browser CDP, create target console.log('[2] Getting browser WS URL...'); const ver = await (await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`)).json(); const browserWsUrl = ver.webSocketDebuggerUrl; const bw = new WebSocket(browserWsUrl); await new Promise((resolve, reject) => { bw.once('open', resolve); bw.once('error', reject); setTimeout(() => reject(new Error('browser WS timeout')), 5000); }); const bcdp = makeCDPHelper(bw); // Create a new page target console.log('[3] Creating page target...'); const targetResult = await bcdp('Target.createTarget', { url: 'about:blank', width: 1440, height: 900 }); const targetId = targetResult?.targetId; const pages = await (await fetch(`http://127.0.0.1:${CDP_PORT}/json`)).json(); const ourPage = pages.find(p => p.id === targetId || p.url === 'about:blank'); const pageWsUrl = ourPage?.webSocketDebuggerUrl; if (!pageWsUrl) throw new Error('Could not find page WS URL'); bw.close(); // Step 4: Connect to page CDP console.log('[4] Connecting to page target...'); const pw = new WebSocket(pageWsUrl); await new Promise((resolve, reject) => { pw.once('open', resolve); pw.once('error', reject); setTimeout(() => reject(new Error('page WS timeout')), 5000); }); const cdp = makeCDPHelper(pw); await cdp('Page.enable'); await cdp('Runtime.enable'); await cdp('DOM.enable'); // Step 5: Navigate, inject auth, reload console.log('[5] Navigating and setting auth...'); await cdp('Page.navigate', { url: BASE }); await sleep(3000); await cdp('Runtime.evaluate', { expression: `localStorage.setItem('token', ${JSON.stringify(token)}); localStorage.setItem('user_id', '${userId}'); 'injected';` }); console.log('[6] Reloading with auth...'); await cdp('Page.navigate', { url: BASE }); await sleep(5000); // Check page state const pageState = await evaluate(cdp, `JSON.stringify({ title: document.title, hasTextarea: !!document.querySelector('textarea'), bodyClasses: document.body.className, rootExists: !!document.getElementById('root') })`, 'PageState' ); console.log(' Page state:', pageState); // Step 7: Screenshot console.log('[7] Taking screenshot...'); const ss = await cdp('Page.captureScreenshot', { format: 'png', clip: { x: 0, y: 0, width: 1440, height: 900, scale: 1 } }); if (ss?.data) { writeFileSync(`${OUT}/screenshot_layout.png`, Buffer.from(ss.data, 'base64')); console.log(' ✅ Screenshot saved'); } // ============ DETAILED DIAGNOSTICS ============ console.log('\n[8] === HEIGHT CHAIN DIAGNOSIS ==='); // 8a: Viewport const vp = await evaluate(cdp, `JSON.stringify({ w: innerWidth, h: innerHeight, scrollY: scrollY, scrollX: scrollX })`, 'Viewport' ); // 8b: Height chain: #root → .h-screen → main → App wrapper → ChatContainer await evaluate(cdp, ` (() => { const results = []; const root = document.getElementById('root'); results.push({ id: '#root', exists: !!root, clientH: root?.clientHeight, scrollH: root?.scrollHeight, cs: root ? getComputedStyle(root).height : 'N/A' }); const hScreen = document.querySelector('.h-screen'); results.push({ id: '.h-screen', exists: !!hScreen, clientH: hScreen?.clientHeight, scrollH: hScreen?.scrollHeight, cs: hScreen ? getComputedStyle(hScreen).height : 'N/A', csDisplay: hScreen ? getComputedStyle(hScreen).display : 'N/A' }); const main = document.querySelector('main'); results.push({ id: 'main', exists: !!main, clientH: main?.clientHeight, scrollH: main?.scrollHeight, cs: main ? getComputedStyle(main).height : 'N/A', csOverflow: main ? getComputedStyle(main).overflow : 'N/A' }); const flexColFull = document.querySelector('.flex.flex-col.h-full.overflow-hidden') || document.querySelector('main > div'); results.push({ id: 'App flex-col', exists: !!flexColFull, clientH: flexColFull?.clientHeight, scrollH: flexColFull?.scrollHeight, className: flexColFull?.className?.slice(0, 200) || 'N/A' }); const chatBg = document.querySelector('.chat-background'); results.push({ id: '.chat-background', exists: !!chatBg, clientH: chatBg?.clientHeight, scrollH: chatBg?.scrollHeight, rect: chatBg ? (()=>{const r=chatBg.getBoundingClientRect(); return {t:r.top,b:r.bottom,h:r.height};})() : null }); const textarea = document.querySelector('textarea'); results.push({ id: 'textarea', exists: !!textarea, rect: textarea ? (()=>{const r=textarea.getBoundingClientRect(); return {t:Math.round(r.top),b:Math.round(r.bottom),l:r.left,r:r.right,w:Math.round(r.width),h:Math.round(r.height)};})() : null, display: textarea ? getComputedStyle(textarea).display : 'N/A', visibility: textarea ? getComputedStyle(textarea).visibility : 'N/A', opacity: textarea ? getComputedStyle(textarea).opacity : 'N/A' }); return JSON.stringify(results, null, 2); })() `, 'HeightChain'); // 8c: ChatInput wrapper and IoTStatusBar wrapper await evaluate(cdp, ` (() => { const results = []; // Find ChatInput wrapper (flex-shrink-0 after flex-1) const flexShrinkDivs = Array.from(document.querySelectorAll('.flex-shrink-0')); for (const div of flexShrinkDivs) { const r = div.getBoundingClientRect(); const hasTextarea = div.querySelector('textarea'); const hasIoT = div.querySelector('button[title*="IoT"]') || div.textContent?.includes('IoT'); results.push({ element: hasTextarea ? 'ChatInput wrapper' : (div.textContent?.includes('IoT') ? 'IoT wrapper' : 'other flex-shrink-0'), className: div.className?.slice(0, 200) || '', rect: { t: Math.round(r.top), b: Math.round(r.bottom), l: Math.round(r.left), h: Math.round(r.height) }, clientH: div.clientHeight, display: getComputedStyle(div).display, visibility: getComputedStyle(div).visibility, hasTextarea: !!hasTextarea, textContent: (div.textContent || '').slice(0, 80), }); } // Find border-t elements (ChatInput has border-t) const borderT = Array.from(document.querySelectorAll('.border-t')); for (const el of borderT) { const r = el.getBoundingClientRect(); results.push({ element: 'border-t element', className: (el.className?.slice(0, 200) || ''), rect: { t: Math.round(r.top), b: Math.round(r.bottom), h: Math.round(r.height) }, clientH: el.clientHeight, display: getComputedStyle(el).display, visible: r.top < innerHeight && r.bottom > 0, textContent: (el.textContent || '').slice(0, 100), }); } return JSON.stringify(results, null, 2); })() `, 'FlexShrinkAndBorder'); // 8d: Direct query for ChatInput and IoTStatusBar components await evaluate(cdp, ` (() => { const results = {}; // All textareas const textareas = Array.from(document.querySelectorAll('textarea')); results.textareas = textareas.map((ta, i) => { const r = ta.getBoundingClientRect(); let parent = ta.parentElement; const parentChain = []; for (let j = 0; j < 6 && parent; j++) { parentChain.push({ tag: parent.tagName, cls: (parent.className?.slice(0, 150) || ''), clientH: parent.clientHeight, scrollH: parent.scrollHeight, display: getComputedStyle(parent).display, overflow: getComputedStyle(parent).overflow, rect: (() => { const pr = parent.getBoundingClientRect(); return { t: Math.round(pr.top), b: Math.round(pr.bottom), h: Math.round(pr.height) }; })(), }); parent = parent.parentElement; } return { idx: i, rect: { t: Math.round(r.top), b: Math.round(r.bottom), l: Math.round(r.left), r: Math.round(r.right), w: Math.round(r.width), h: Math.round(r.height) }, clientH: ta.clientHeight, display: getComputedStyle(ta).display, visibility: getComputedStyle(ta).visibility, opacity: getComputedStyle(ta).opacity, isVisible: r.top < innerHeight && r.bottom > 0 && r.width > 0 && r.height > 0, parentChain, }; }); // All divs with IoT-related text const allDivs = Array.from(document.querySelectorAll('div')); results.iotDivs = allDivs .filter(d => (d.textContent || '').includes('IoT')) .map(d => { const r = d.getBoundingClientRect(); return { text: (d.textContent || '').slice(0, 100), cls: (d.className?.slice(0, 200) || ''), rect: { t: Math.round(r.top), b: Math.round(r.bottom), h: Math.round(r.height) }, clientH: d.clientHeight, display: getComputedStyle(d).display, visibility: getComputedStyle(d).visibility, isVisible: r.top < innerHeight && r.bottom > 0 && r.width > 0, }; }); // Check if #root children are rendered const root = document.getElementById('root'); if (root) { results.rootChildren = Array.from(root.children).map((c, i) => ({ idx: i, tag: c.tagName, cls: (c.className?.slice(0, 200) || ''), childCount: c.children.length, clientH: c.clientHeight, rect: (() => { const r = c.getBoundingClientRect(); return { t: Math.round(r.top), b: Math.round(r.bottom), h: Math.round(r.height) }; })(), })); } return JSON.stringify(results, null, 2); })() `, 'DetailedElements'); // 8e: Check what's inside main element await evaluate(cdp, ` (() => { const main = document.querySelector('main'); if (!main) return 'NO MAIN ELEMENT FOUND!'; const results = { mainClientH: main.clientHeight, mainChildCount: main.children.length, children: Array.from(main.children).map((c, i) => ({ idx: i, tag: c.tagName, cls: (c.className?.slice(0, 200) || ''), clientH: c.clientHeight, scrollH: c.scrollHeight, rect: (() => { const r = c.getBoundingClientRect(); return { t: Math.round(r.top), b: Math.round(r.bottom), h: Math.round(r.height) }; })(), display: getComputedStyle(c).display, overflow: getComputedStyle(c).overflow, childCount: c.children.length, grandchildren: Array.from(c.children).map((gc, j) => ({ idx: j, tag: gc.tagName, cls: (gc.className?.slice(0, 200) || ''), clientH: gc.clientHeight, rect: (() => { const r = gc.getBoundingClientRect(); return { t: Math.round(r.top), b: Math.round(r.bottom), h: Math.round(r.height) }; })(), })), })), }; return JSON.stringify(results, null, 2); })() `, 'MainChildren'); // Write diagnostics console.log('\n[9] Collecting final diagnostics...'); const finalDiag = await evaluate(cdp, ` (() => { const D = {}; D.viewport = { w: innerWidth, h: innerHeight }; D.chatInputVisible = (() => { const ta = document.querySelector('textarea'); if (!ta) return { reason: 'NO_TEXTAREA_FOUND' }; const r = ta.getBoundingClientRect(); const cs = getComputedStyle(ta); return { rect: { t: Math.round(r.top), b: Math.round(r.bottom), l: Math.round(r.left), r: Math.round(r.right), w: Math.round(r.width), h: Math.round(r.height) }, display: cs.display, visibility: cs.visibility, opacity: cs.opacity, isInViewport: r.top < innerHeight && r.bottom > 0, isBelowFold: r.top >= innerHeight, isAboveFold: r.bottom <= 0, hasSize: r.width > 0 && r.height > 0, }; })(); D.iotStatusBarVisible = (() => { const iotDivs = Array.from(document.querySelectorAll('div')).filter(d => (d.textContent || '').includes('IoT 设备')); if (iotDivs.length === 0) return { reason: 'NO_IOT_DIV_FOUND' }; const el = iotDivs[0]; const r = el.getBoundingClientRect(); const cs = getComputedStyle(el); return { rect: { t: Math.round(r.top), b: Math.round(r.bottom), h: Math.round(r.height) }, display: cs.display, visibility: cs.visibility, opacity: cs.opacity, isInViewport: r.top < innerHeight && r.bottom > 0, isBelowFold: r.top >= innerHeight, }; })(); // Check all parent heights D.parentHeights = []; const root = document.getElementById('root'); if (root) { let el = root; let depth = 0; while (el && depth < 10) { D.parentHeights.push({ depth, tag: el.tagName, cls: (el.className?.slice(0, 150) || ''), clientH: el.clientHeight, scrollH: el.scrollHeight, csHeight: getComputedStyle(el).height, csOverflow: getComputedStyle(el).overflow, csDisplay: getComputedStyle(el).display, }); el = el.children[0]; depth++; } } // Check what's in the main content area const main = document.querySelector('main'); if (main) { D.mainInfo = { clientH: main.clientHeight, scrollH: main.scrollHeight, csHeight: getComputedStyle(main).height, csOverflow: getComputedStyle(main).overflow, childCount: main.children.length, childTags: Array.from(main.children).map(c => ({ tag: c.tagName, cls: (c.className?.slice(0, 100) || ''), clientH: c.clientHeight })), }; } return JSON.stringify(D, null, 2); })() `, 'FinalDiagnosis'); writeFileSync(`${OUT}/diagnostics.json`, JSON.stringify(JSON.parse(finalDiag), null, 2)); console.log('\n✅ Diagnostics saved to diagnostics.json'); pw.close(); console.log('Done.'); } main().catch(err => { console.error('Fatal:', err); process.exit(1); });