chore: 界面诊断脚本入库;忽略 _backup/output 现场目录
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* UI-level diagnosis for motor console (no backend-side health APIs as truth).
|
||||
* Launches Edge against :2625/pages/motor.html, dumps what the page SHOWS.
|
||||
* Usage: node scripts/ui_diag_motor.mjs [port]
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const PORT = Number(process.argv[2] || 2625);
|
||||
const PAGE = `http://127.0.0.1:${PORT}/pages/motor.html`;
|
||||
const CDP = "http://127.0.0.1:9333";
|
||||
const OUT = path.resolve("output/ui_diag");
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
async function findWs() {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
try {
|
||||
const list = await (await fetch(CDP + "/json/list")).json();
|
||||
const page = list.find((t) => t.type === "page" && t.url.includes("motor"));
|
||||
if (page && page.webSocketDebuggerUrl) return page.webSocketDebuggerUrl;
|
||||
const any = list.find((t) => t.type === "page");
|
||||
if (any && any.webSocketDebuggerUrl) return any.webSocketDebuggerUrl;
|
||||
} catch (_) {}
|
||||
await sleep(250);
|
||||
}
|
||||
throw new Error("CDP not available on 9333");
|
||||
}
|
||||
|
||||
const edgeCandidates = [
|
||||
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
||||
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
||||
];
|
||||
|
||||
function launchEdge() {
|
||||
const exe = edgeCandidates.find((p) => p && require("fs").existsSync(p)) || "msedge.exe";
|
||||
// prefer spawn without require - use existsSync from fs
|
||||
return spawn(
|
||||
edgeCandidates.find((p) => {
|
||||
try {
|
||||
return require("node:fs").existsSync(p);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}) || "msedge.exe",
|
||||
[
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
`--remote-debugging-port=9333`,
|
||||
"--user-data-dir=" + path.join(OUT, "edge-profile"),
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
PAGE,
|
||||
],
|
||||
{ stdio: "ignore", detached: true }
|
||||
);
|
||||
}
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
|
||||
function launchEdge2() {
|
||||
const exe =
|
||||
edgeCandidates.find((p) => existsSync(p)) || "msedge.exe";
|
||||
const child = spawn(
|
||||
exe,
|
||||
[
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--remote-debugging-port=9333",
|
||||
`--user-data-dir=${path.join(OUT, "edge-profile")}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
PAGE,
|
||||
],
|
||||
{ stdio: "ignore", detached: true }
|
||||
);
|
||||
child.unref();
|
||||
return child;
|
||||
}
|
||||
|
||||
const url = await (async () => {
|
||||
try {
|
||||
await fetch(CDP + "/json/list");
|
||||
} catch {
|
||||
console.log("launching Edge headless →", PAGE);
|
||||
launchEdge2();
|
||||
await sleep(2000);
|
||||
}
|
||||
return findWs();
|
||||
})();
|
||||
|
||||
console.log("CDP", url);
|
||||
const sock = new WebSocket(url);
|
||||
await new Promise((r, j) => {
|
||||
sock.onopen = r;
|
||||
sock.onerror = j;
|
||||
});
|
||||
let id = 0;
|
||||
const pending = new Map();
|
||||
const consoleMsgs = [];
|
||||
sock.onmessage = (ev) => {
|
||||
const m = JSON.parse(ev.data);
|
||||
if (m.id && pending.has(m.id)) {
|
||||
pending.get(m.id)(m);
|
||||
pending.delete(m.id);
|
||||
}
|
||||
if (m.method === "Runtime.consoleAPICalled") {
|
||||
const args = (m.params.args || [])
|
||||
.map((a) => a.value ?? a.description ?? a.unserializableValue ?? "")
|
||||
.join(" ");
|
||||
consoleMsgs.push(`[${m.params.type}] ${args}`);
|
||||
}
|
||||
if (m.method === "Runtime.exceptionThrown") {
|
||||
const d = m.params.exceptionDetails || {};
|
||||
consoleMsgs.push(
|
||||
`[exception] ${d.text || ""} ${d.exception && d.exception.description ? d.exception.description : ""}`
|
||||
);
|
||||
}
|
||||
if (m.method === "Log.entryAdded") {
|
||||
const e = m.params.entry || {};
|
||||
consoleMsgs.push(`[log:${e.level}] ${e.text}`);
|
||||
}
|
||||
};
|
||||
function send(method, params = {}) {
|
||||
const myId = ++id;
|
||||
return new Promise((res) => {
|
||||
pending.set(myId, res);
|
||||
sock.send(JSON.stringify({ id: myId, method, params }));
|
||||
});
|
||||
}
|
||||
async function evaluate(expr) {
|
||||
const r = await send("Runtime.evaluate", {
|
||||
expression: expr,
|
||||
awaitPromise: true,
|
||||
returnByValue: true,
|
||||
});
|
||||
if (r.result && r.result.exceptionDetails) {
|
||||
return {
|
||||
__err: JSON.stringify(r.result.exceptionDetails).slice(0, 500),
|
||||
};
|
||||
}
|
||||
return r.result && r.result.result && r.result.result.value;
|
||||
}
|
||||
|
||||
await send("Runtime.enable");
|
||||
await send("Log.enable");
|
||||
await send("Page.enable");
|
||||
await send("Page.navigate", { url: PAGE });
|
||||
await sleep(4000);
|
||||
|
||||
const report = await evaluate(`(() => {
|
||||
const $ = (s) => document.querySelector(s);
|
||||
const txt = (s) => { const e=$(s); return e ? (e.textContent||'').trim() : null; };
|
||||
const cls = (s) => { const e=$(s); return e ? e.className : null; };
|
||||
const nodes = [...document.querySelectorAll('#nodeList .node-item, #nodeList .node, #nodeList [data-node], #nodeList button, #nodeList .node-row')].map(e => ({
|
||||
tag: e.tagName,
|
||||
cls: e.className,
|
||||
text: (e.textContent||'').trim().slice(0,80),
|
||||
html: (e.innerHTML||'').slice(0,120)
|
||||
}));
|
||||
const statusPanel = $('#statusPanel, .status-panel, #statusBody');
|
||||
const sb = {};
|
||||
for (const id of ['sbLastSend','sbRx','sbTx','sbErr','devStatus','ipLabel','connLed','nodeBadge','netBannerText','refreshRate']) {
|
||||
const e = document.getElementById(id);
|
||||
sb[id] = e ? {text:(e.textContent||'').trim().slice(0,80), cls:e.className, value:e.value??null, title:e.title||null} : null;
|
||||
}
|
||||
let dbg = null;
|
||||
try {
|
||||
const s = window.motorDbg && window.motorDbg.state;
|
||||
if (s) {
|
||||
dbg = {
|
||||
connected: s.connected,
|
||||
connecting: s.connecting,
|
||||
probing: s.probing,
|
||||
tpId: s.tpId,
|
||||
ip: s.ip,
|
||||
probeErr: s.probeErr,
|
||||
probeErrFull: (s.probeErrFull||'').slice(0,200),
|
||||
failCount: s.failCount,
|
||||
refreshIntervalMs: s.refreshIntervalMs,
|
||||
motorsLen: (s.motors||[]).length,
|
||||
motorsSample: (s.motors||[]).slice(0,3).map(m=>({ch:m.ch,node:m.node,online:m.online,dlc:m.dlc,dataLen:(m.data||'').length})),
|
||||
chOnline: s.chOnline,
|
||||
chEnabled: s.chEnabled,
|
||||
selectedNode: s.selectedNode,
|
||||
decodedSize: s.decoded && s.decoded.size,
|
||||
recvTsLen: (s.recvTs||[]).length,
|
||||
tickStuck: s.tickStuck,
|
||||
txCount: s.txCount,
|
||||
transportHas: !!(s.tp),
|
||||
transportId: s.tp && s.tp.id,
|
||||
lastConnToastAt: s.lastConnToastAt,
|
||||
};
|
||||
}
|
||||
} catch(e) { dbg = {err:String(e)}; }
|
||||
|
||||
// overlay / empty states visible to user
|
||||
const overlays = [...document.querySelectorAll('.offline-overlay,.empty-state,.status-offline,#statusEmpty,.panel-empty')]
|
||||
.map(e => ({cls:e.className, text:(e.textContent||'').trim().slice(0,100), hidden:e.hidden, display:getComputedStyle(e).display}));
|
||||
|
||||
// any toast visible
|
||||
const toasts = [...document.querySelectorAll('#toastContainer .toast, .toast')]
|
||||
.map(e => ({cls:e.className, text:(e.textContent||'').trim().slice(0,80)}));
|
||||
|
||||
return {
|
||||
title: document.title,
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
bodyClass: document.body && document.body.className,
|
||||
htmlData: document.documentElement.dataset,
|
||||
toolbar: {
|
||||
brand: txt('.toolbar-brand span'),
|
||||
sub: txt('.toolbar-brand .sub'),
|
||||
devStatus: txt('#devStatus'),
|
||||
connLedCls: cls('#connLed'),
|
||||
ipLabel: txt('#ipLabel'),
|
||||
transportSelect: document.querySelector('#transportSelect, #linkSelect, select[id*=transport], select[id*=link]')?.value,
|
||||
transportOptions: [...document.querySelectorAll('select option')].map(o=>o.value+':'+o.textContent.trim()).slice(0,12),
|
||||
},
|
||||
sb,
|
||||
nodeListCount: nodes.length,
|
||||
nodes,
|
||||
statusPanelText: statusPanel ? (statusPanel.textContent||'').trim().slice(0,400) : null,
|
||||
overlays,
|
||||
toasts,
|
||||
dbg,
|
||||
scripts: [...document.scripts].map(s=>s.src).filter(Boolean),
|
||||
};
|
||||
})()`);
|
||||
|
||||
const consoleTail = consoleMsgs.slice(-40);
|
||||
const dump = { when: new Date().toISOString(), PAGE, report, consoleTail };
|
||||
const outFile = path.join(OUT, "ui_diag.json");
|
||||
writeFileSync(outFile, JSON.stringify(dump, null, 2), "utf8");
|
||||
console.log("WROTE", outFile);
|
||||
console.log(JSON.stringify({ title: report && report.title, toolbar: report && report.toolbar, sb: report && report.sb, nodeListCount: report && report.nodeListCount, nodes: report && report.nodes, overlays: report && report.overlays, toasts: report && report.toasts, dbg: report && report.dbg, scripts: report && report.scripts, consoleTail }, null, 2));
|
||||
if (report && report.__err) console.log("EVAL_ERR", report.__err);
|
||||
sock.close();
|
||||
process.exit(0);
|
||||
Reference in New Issue
Block a user