gui: 性能修复(星图/reasoning/渲染循环)+设备桥配置解耦(独立prefs+IPC)

性能(卡死修复, 已部署本机验证 GPU 91%->14%):
- reasoning_content 懒加载: 折叠正文不再 marked.parse, 点击展开时才解析
- 星图动画仅"星图"子面板激活才跑, 12fps限制, 关抗锯齿+pixelRatio=1, 粒子3000->1200
- renderAll/refreshAll 非chat视图跳过聊天整块重建
- switchView(chat) 多段延迟滚动到底部(修复进入停在顶部)

设备桥(配置与连接解耦, 为远程homed被控做准备):
- 设备桥改由 gui-prefs.json 的 deviceBridge{enabled,gateway,token} 驱动, 不再依赖 type=device 连接
- 新增 IPC device-bridge:get/set (状态查询+动态启停), preload 暴露 deviceBridge API
- whenReady 按 prefs 启动设备桥

待后续: 设备页UI入口(renderDevices 本机卡片仍被 conn.type!==device 条件挡住, 需改为始终显示+调用 device-bridge:set)
This commit is contained in:
2026-08-17 17:35:47 +08:00
parent a9ec8f5b2b
commit 5a8fda955a
3 changed files with 120 additions and 31 deletions

View File

@ -1,4 +1,4 @@
const { app, BrowserWindow, ipcMain, dialog, Menu } = require("electron"); const { app, BrowserWindow, ipcMain, Menu } = require("electron");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const { spawn } = require("child_process"); const { spawn } = require("child_process");
@ -1032,11 +1032,17 @@ app.whenReady().then(async () => {
console.error("homed failed to start within timeout"); console.error("homed failed to start within timeout");
} }
} }
// 设备桥:启动接入 remotedevice // 设备桥:由 GUI 偏好deviceBridge 开关+网关+token驱动独立于连接
try { try {
var _devs = loadConnections().connections || []; const _prefs = loadGuiPrefs();
var _dev = _devs.filter((c) => c.type === "device"); const _db = _prefs.deviceBridge || {};
if (_dev.length > 0) startDeviceBridge(_dev[_dev.length - 1]); if (_db.enabled && _db.gateway && _db.token) {
startDeviceBridge({ url: _db.gateway, apiKey: _db.token });
} else if (_db.enabled) {
console.error(
"[device-bridge] enabled but missing gateway/token, skipped",
);
}
} catch (e) { } catch (e) {
console.error("device bridge init: " + e.message); console.error("device bridge init: " + e.message);
} }
@ -1096,14 +1102,25 @@ function loadGuiPrefs() {
try { try {
if (fs.existsSync(GUI_PREFS_FILE)) { if (fs.existsSync(GUI_PREFS_FILE)) {
const d = JSON.parse(fs.readFileSync(GUI_PREFS_FILE, "utf-8")); const d = JSON.parse(fs.readFileSync(GUI_PREFS_FILE, "utf-8"));
const db = d.deviceBridge || {};
return { return {
autoLaunch: !!d.autoLaunch, autoLaunch: !!d.autoLaunch,
silentStart: !!d.silentStart, silentStart: !!d.silentStart,
exitToTray: d.exitToTray === undefined ? true : !!d.exitToTray, exitToTray: d.exitToTray === undefined ? true : !!d.exitToTray,
deviceBridge: {
enabled: !!db.enabled,
gateway: db.gateway || "",
token: db.token || "",
},
}; };
} }
} catch (e) {} } catch (e) {}
return { autoLaunch: false, silentStart: false, exitToTray: true }; return {
autoLaunch: false,
silentStart: false,
exitToTray: true,
deviceBridge: { enabled: false, gateway: "", token: "" },
};
} }
function saveGuiPrefs(p) { function saveGuiPrefs(p) {
@ -1131,7 +1148,45 @@ function applyAutoLaunch(enabled) {
// 全局偏好缓存(供 createWindow 静默判断使用) // 全局偏好缓存(供 createWindow 静默判断使用)
let guiPrefs = loadGuiPrefs(); let guiPrefs = loadGuiPrefs();
// IPC本机设备身份(设备桥登记的设备 ID 与状态) // IPC本机设备桥状态(启用+网关+token+连接状态)
ipcMain.handle("device-bridge:get", () => {
const p = loadGuiPrefs();
const db = p.deviceBridge || {};
return {
enabled: !!db.enabled,
gateway: db.gateway || "",
tokenSet: !!(db.token || ""),
connected: !!deviceBridge,
deviceId: deviceBridgeId,
address: deviceBridgeAddr,
};
});
// IPC配置本机设备桥开关+网关+token保存并动态启停
ipcMain.handle("device-bridge:set", (_, cfg) => {
const cur = loadGuiPrefs();
const db = Object.assign({}, cur.deviceBridge || {}, cfg || {});
const next = Object.assign({}, cur, { deviceBridge: db });
guiPrefs = saveGuiPrefs(next);
// 动态应用:停止现有桥,按新配置启动
try {
stopDeviceBridge();
} catch (e) {}
if (db.enabled && db.gateway && db.token) {
try {
startDeviceBridge({ url: db.gateway, apiKey: db.token });
} catch (e) {
console.error("[device-bridge] start failed: " + e.message);
}
}
return {
enabled: !!db.enabled,
gateway: db.gateway || "",
connected: !!deviceBridge,
deviceId: deviceBridgeId,
address: deviceBridgeAddr,
};
});
// IPC读取偏好 // IPC读取偏好
ipcMain.handle("prefs:get", () => { ipcMain.handle("prefs:get", () => {

View File

@ -41,4 +41,8 @@ contextBridge.exposeInMainWorld("homeagent", {
device: { device: {
identity: () => ipcRenderer.invoke("device:identity"), identity: () => ipcRenderer.invoke("device:identity"),
}, },
deviceBridge: {
get: () => ipcRenderer.invoke("device-bridge:get"),
set: (cfg) => ipcRenderer.invoke("device-bridge:set", cfg),
},
}); });

View File

@ -475,16 +475,24 @@ function switchView(n) {
state.currentView = n; state.currentView = n;
if (n === "chat") { if (n === "chat") {
state.chatStick = true; state.chatStick = true;
var msgsEl = document.getElementById("chat-msgs"); var scrollToBottom = () => {
if (msgsEl) { var msgsEl = document.getElementById("chat-msgs");
if (!msgsEl) return;
try { try {
msgsEl.scrollTo({ top: msgsEl.scrollHeight, behavior: "smooth" });
} catch (e) {
msgsEl.scrollTop = msgsEl.scrollHeight; msgsEl.scrollTop = msgsEl.scrollHeight;
} } catch (e) {}
} };
// 立即滚 + 渲染完成后/延迟再滚DOM 重建后会重置滚动位置)
scrollToBottom();
setTimeout(scrollToBottom, 60);
setTimeout(scrollToBottom, 300);
} }
renderAll(); renderAll();
if (n === "chat") {
// renderAll含异步 refreshAll完成后确保仍在底部
setTimeout(scrollToBottom, 600);
setTimeout(scrollToBottom, 1500);
}
} }
// ===== Tab Render Dispatch ===== // ===== Tab Render Dispatch =====
@ -499,15 +507,14 @@ function renderAll() {
console.error("renderOverview", e); console.error("renderOverview", e);
} }
try { try {
renderChat(); // 聊天视图未激活时不重建聊天 DOM避免每次轮询/切换全量重建→卡死)
if (!state.currentView || state.currentView === "chat") {
renderChat();
renderChatStarmap();
}
} catch (e) { } catch (e) {
console.error("renderChat", e); console.error("renderChat", e);
} }
try {
renderChatStarmap();
} catch (e) {
console.error("renderChatStarmap", e);
}
try { try {
renderPlugins(); renderPlugins();
} catch (e) { } catch (e) {
@ -593,15 +600,14 @@ async function refreshAll() {
console.error("renderOverview", e); console.error("renderOverview", e);
} }
try { try {
renderChat(); // 聊天视图未激活时不重建聊天 DOM避免每次轮询/切换全量重建→卡死)
if (!state.currentView || state.currentView === "chat") {
renderChat();
renderChatStarmap();
}
} catch (e) { } catch (e) {
console.error("renderChat", e); console.error("renderChat", e);
} }
try {
renderChatStarmap();
} catch (e) {
console.error("renderChatStarmap", e);
}
try { try {
renderPlugins(); renderPlugins();
} catch (e) { } catch (e) {
@ -1028,9 +1034,7 @@ function renderReasoningCard(text, isStreaming, idx) {
? '<div class="reasoning-preview">' + ? '<div class="reasoning-preview">' +
escHtml(preview) + escHtml(preview) +
'</div><div class="reasoning-sweep"></div>' '</div><div class="reasoning-sweep"></div>'
: '<div class="reasoning-content">' + : '<div class="reasoning-content"></div>') +
(typeof marked === "undefined" ? escHtml(text) : marked.parse(text)) +
"</div>") +
"</div></div>" "</div></div>"
); );
} }
@ -1508,9 +1512,10 @@ function initChatStarmap() {
starmapScene.fog = new THREE.FogExp2(0x0a0a1a, 0.015); starmapScene.fog = new THREE.FogExp2(0x0a0a1a, 0.015);
starmapCam = new THREE.PerspectiveCamera(60, w / h, 0.1, 2000); starmapCam = new THREE.PerspectiveCamera(60, w / h, 0.1, 2000);
starmapCam.position.set(0, 20, 40); starmapCam.position.set(0, 20, 40);
starmapRen = new THREE.WebGLRenderer({ antialias: true, alpha: true }); // 性能:关抗锯齿 + pixelRatio 1装饰背景视觉差异极小GPU 负载显著下降)
starmapRen = new THREE.WebGLRenderer({ antialias: false, alpha: true });
starmapRen.setSize(w, h); starmapRen.setSize(w, h);
starmapRen.setPixelRatio(Math.min(window.devicePixelRatio, 2)); starmapRen.setPixelRatio(1);
starmapRen.setClearColor(0x0a0a1a, 1); starmapRen.setClearColor(0x0a0a1a, 1);
cont.innerHTML = ""; cont.innerHTML = "";
cont.appendChild(starmapRen.domElement); cont.appendChild(starmapRen.domElement);
@ -1531,6 +1536,11 @@ function initChatStarmap() {
starmapRen.domElement.addEventListener("click", onStarmapClick); starmapRen.domElement.addEventListener("click", onStarmapClick);
window.addEventListener("resize", onStarmapResize); window.addEventListener("resize", onStarmapResize);
if (starmapRaf) cancelAnimationFrame(starmapRaf); if (starmapRaf) cancelAnimationFrame(starmapRaf);
// 首次加载强制渲染一帧(即使星图面板未激活,切换过去也有内容)
try {
if (starmapRen && starmapScene && starmapCam)
starmapRen.render(starmapScene, starmapCam);
} catch (e) {}
starmapAnimate(); starmapAnimate();
} }
@ -1984,6 +1994,11 @@ function switchChatPanel(tab, el) {
if (tab === "starmap") { if (tab === "starmap") {
renderChatStarmap(); renderChatStarmap();
onStarmapResize(); onStarmapResize();
// 星图面板激活:若动画未在跑则启动
if (!starmapRaf) {
starmapLastFrame = 0;
starmapAnimate();
}
} }
if (tab === "terminal") renderTerminals(); if (tab === "terminal") renderTerminals();
if (tab === "cmd") renderCmdHistory(); if (tab === "cmd") renderCmdHistory();
@ -2753,7 +2768,7 @@ var smEdgeColors = {
}; };
function createStarField() { function createStarField() {
var c = 3000; var c = 1200;
var p = new Float32Array(c * 3), var p = new Float32Array(c * 3),
cl = new Float32Array(c * 3), cl = new Float32Array(c * 3),
s = new Float32Array(c); s = new Float32Array(c);
@ -2915,8 +2930,23 @@ function resetStarmapCamera() {
})(); })();
} }
var starmapLastFrame = 0;
function starmapPanelActive() {
// 仅当 chat 视图且"星图"子面板激活时才运行动画
if (!state.currentView || state.currentView !== "chat") return false;
var panel = document.getElementById("chat-panel-starmap");
return panel && panel.classList.contains("active");
}
function starmapAnimate() { function starmapAnimate() {
if (!starmapPanelActive()) {
starmapRaf = null;
return;
}
starmapRaf = requestAnimationFrame(starmapAnimate); starmapRaf = requestAnimationFrame(starmapAnimate);
// 帧率限制 ~12fps装饰背景无需 60fpsGPU 高占用主因)
var now = Date.now();
if (now - starmapLastFrame < 83) return;
starmapLastFrame = now;
if (starmapCtrl) starmapCtrl.update(); if (starmapCtrl) starmapCtrl.update();
if (starmapStarField) starmapStarField.rotation.y += 0.0001; if (starmapStarField) starmapStarField.rotation.y += 0.0001;
if (starmapRen && starmapScene && starmapCam) if (starmapRen && starmapScene && starmapCam)