a058b0ab8e
- 修复记忆管理数据库连接不可用 (ai-core重编译+Unicode修复) - 修复IoT子会话工具调用链路日志缺失 - 新增最终审查子会话(review_provider) 支持消息格式解析拆分 - 实现历史消息持久化(后端存储+前端分页加载) - 前端新增动作消息(ActionMessage)类型和渲染 - 优化对话链路速度(非阻塞子会话+快速问候通道) - JWT密钥环境变量化(无默认值启动panic) - Token自动刷新机制(401拦截器+refresh接口) - WebSocket指数退避重连(jitter+最大10次) - localStorage清理一致性(cyrene_前缀+版本检查) - IoT环境变量统一为IOT_SERVICE_URL
414 lines
16 KiB
JavaScript
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);
|
|
});
|