Files
Cyrene/debug/diagnose_layout_v4.mjs
AskaEth a058b0ab8e fix: 第一轮修复 - 记忆管理/IoT操控/历史消息持久化/动作消息/链路优化/安全配置
- 修复记忆管理数据库连接不可用 (ai-core重编译+Unicode修复)
- 修复IoT子会话工具调用链路日志缺失
- 新增最终审查子会话(review_provider) 支持消息格式解析拆分
- 实现历史消息持久化(后端存储+前端分页加载)
- 前端新增动作消息(ActionMessage)类型和渲染
- 优化对话链路速度(非阻塞子会话+快速问候通道)
- JWT密钥环境变量化(无默认值启动panic)
- Token自动刷新机制(401拦截器+refresh接口)
- WebSocket指数退避重连(jitter+最大10次)
- localStorage清理一致性(cyrene_前缀+版本检查)
- IoT环境变量统一为IOT_SERVICE_URL
2026-05-21 23:10:07 +08:00

414 lines
16 KiB
JavaScript

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