gui: 自动重登/定时授权/副屏修复 + 排障(大量JS报错根因是cookie过期)

排障: "大量JS报错"真实根因 = webui cookie 会话24h过期 → 所有 api() 请求经网关 302 → 返回登录页HTML(200非401) → renderer 拿HTML当JSON解析 → 界面持续报错/乱码

修复(api() 自动重登):
- 401 或检测登录页HTML(THEME_PLACEHOLDER/统一门户登录)时自动 syncConnAuth 重登后重试一次
- 防递归锁 _haReloginLock
- JSON解析容错(HTML不再误报)

定时撤销/恢复授权:
- prefs.deviceBridge.authSchedule {enabled, revokeTime, restoreTime}, 支持跨天(23:00-07:00)
- 主进程每分钟检查, 到点经 webui 反代 /api/v1/device/auth 撤销/恢复(防抖状态机)
- 设备页UI: 开启开关 + 撤销/恢复时间输入(保存并应用生效)
- setDeviceAuthorized 优先 authRule, 否则从 connections.json 取 URL+cookie

副屏修复:
- displays:list 对空 label(' ') trim 后显示 显示器N
- 修复 loadGuiPrefs 只返回3字段导致 screensueDisplay/exec/authSchedule 被抹掉(保存后变默认的根因)

其他:
- 移除远程调试端口(9223, 排障用)
- 保留 window.onerror/onunhandledrejection 透传(gui.log 可见 JS 错误, 便于运维)
This commit is contained in:
2026-08-20 13:29:21 +08:00
parent c29abe9569
commit 257ff0ad5d
2 changed files with 298 additions and 13 deletions

View File

@ -1,3 +1,22 @@
// 全局 JS 错误捕获(透传到 gui.log 便于诊断)
window.onerror = (msg, src, line, col, err) => {
try {
if (window.homeagent && window.homeagent.log)
window.homeagent.log(
"JS-ERR " + msg + " @ " + src + ":" + line + ":" + col,
);
} catch (e) {}
};
window.onunhandledrejection = (e) => {
try {
if (window.homeagent && window.homeagent.log)
window.homeagent.log(
"JS-REJ " +
(e && e.reason ? String(e.reason).slice(0, 200) : "unknown"),
);
} catch (e2) {}
};
// ===== State =====
const state = {
status: {},
@ -466,11 +485,44 @@ async function api(p, o) {
throw new Error(__("连接超时或失败", "Timeout or connection failed"));
}
clearTimeout(timer);
if (r.status === 401) throw new Error(__("认证失败", "unauthorized"));
if (r.status === 401) {
// 认证失败:尝试自动重新登录一次(避免 cookie 过期后界面持续报错)
if (window._haReloginLock) throw new Error(__("认证失败", "unauthorized"));
window._haReloginLock = true;
try {
await syncConnAuth();
await new Promise((res2) => setTimeout(res2, 800));
} catch (e2) {}
window._haReloginLock = false;
// 重试一次
return api(p, o);
}
if (opts.raw) return r;
var body = await r.text();
// 网关会话过期:返回 200 但内容为登录页 HTML —— 自动重登后重试
if (
body.indexOf("THEME_PLACEHOLDER") !== -1 ||
body.indexOf("统一门户登录") !== -1 ||
(body.indexOf("<title>") !== -1 && body.indexOf("login") !== -1)
) {
if (window._haReloginLock) throw new Error(__("认证失败", "unauthorized"));
window._haReloginLock = true;
try {
await syncConnAuth();
await new Promise((res2) => setTimeout(res2, 800));
} catch (e2) {}
window._haReloginLock = false;
return api(p, o);
}
var ct = r.headers.get("content-type") || "";
if (ct.includes("json")) return r.json();
return r.text();
if (ct.includes("json")) {
try {
return JSON.parse(body);
} catch (e3) {
return body;
}
}
return body;
}
// ===== Navigation =====
@ -4899,6 +4951,7 @@ function renderDevices() {
}
var curGateway = dbc.gateway || webuiUrl || "";
var dbExec = dbc.exec || {};
var dbAuthSchedule = dbc.authSchedule || {};
var dispIdx = dbc.screensueDisplay || "0";
var dispOpts = (state.displays || [])
.map(
@ -4974,6 +5027,22 @@ function renderDevices() {
'" placeholder="沙箱目录" style="flex:1;min-width:120px;font-size:13px;padding:3px 6px;border-radius:4px;border:1px solid var(--border-color);background:var(--bg-input);color:var(--text-primary)">'
: '<input id="dev-bridge-boxdir" style="display:none">') +
"</div>" +
// 定时撤销/恢复授权(睡眠期间自动撤销,醒来自动恢复)
'<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">' +
"<label style='font-size:13px'>" +
__("定时撤销授权", "Scheduled revoke") +
'</label><label class="switch" style="margin-right:4px"><input type="checkbox" id="dev-auth-sched-enabled" ' +
(dbAuthSchedule && dbAuthSchedule.enabled ? "checked" : "") +
"><span></span></label>" +
__("撤销", "Revoke") +
' <input type="time" id="dev-auth-revoke" value="' +
escHtml((dbAuthSchedule && dbAuthSchedule.revokeTime) || "") +
'" style="font-size:13px;padding:2px 6px;border-radius:4px;border:1px solid var(--border-color);background:var(--bg-input);color:var(--text-primary)">' +
__("恢复", "Restore") +
' <input type="time" id="dev-auth-restore" value="' +
escHtml((dbAuthSchedule && dbAuthSchedule.restoreTime) || "") +
'" style="font-size:13px;padding:2px 6px;border-radius:4px;border:1px solid var(--border-color);background:var(--bg-input);color:var(--text-primary)">' +
"</div>" +
'<button class="btn btn-ghost btn-sm" onclick="saveBridgeChannel()">' +
__("保存并应用", "Save & Apply") +
"</button></div></div>";
@ -5080,6 +5149,15 @@ async function saveBridgeChannel() {
if (sandbox) ex.sandbox = sandbox.value || "off";
if (boxdir) ex.boxDir = boxdir.value.trim();
cfg.exec = ex;
// 定时撤销/恢复授权
var schedEnabled = document.getElementById("dev-auth-sched-enabled");
var schedRevoke = document.getElementById("dev-auth-revoke");
var schedRestore = document.getElementById("dev-auth-restore");
cfg.authSchedule = {
enabled: !!(schedEnabled && schedEnabled.checked),
revokeTime: schedRevoke ? schedRevoke.value || "" : "",
restoreTime: schedRestore ? schedRestore.value || "" : "",
};
var r = await window.homeagent.deviceBridge.set(cfg);
state.dbConfig = r || state.dbConfig;
toast(__("设备通道已保存并应用", "Device channel saved & applied"));