feat: synchronize CH1 vehicle control and feedback

This commit is contained in:
vee
2026-08-11 10:42:13 +08:00
parent 3701192a11
commit 32379a1573
50 changed files with 13636 additions and 736 deletions
+397 -79
View File
@@ -3,10 +3,17 @@
const STORAGE_KEY = "cognitive-agent.ir-toy-console.v5";
const MODE_KEY = "cognitive-agent.extension-mode";
const CONVERSATION_KEY = "cognitive-agent.conversation-id";
const SLOT_MIN = 1;
const SLOT_MAX = 248;
const ACTION_PAGE_SIZE = 6;
const HISTORY_LIMIT = 40;
const MAX_PLAN_STEPS = 12;
const MIN_PLAN_DELAY_MS = 1;
const MAX_PLAN_DELAY_MS = 30000;
const MAX_TOTAL_PLAN_DELAY_MS = 60000;
const HEALTH_POLL_INTERVAL_MS = 3000;
const HEALTH_REQUEST_TIMEOUT_MS = 5000;
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
@@ -145,41 +152,45 @@
};
const VEHICLE_DEVICES = [
{ name: "座椅", code: "SEAT · CAN", icon: "armchair", actions: [
{ name: "主驾通风", command: "打开主驾座椅通风" },
{ name: "主驾加热", command: "打开主驾座椅加热" },
{ name: "副驾通风", command: "打开副驾座椅通风" },
{ name: "座椅位置", command: "把副驾座椅向后调一点" },
{ name: "主驾座椅", code: "SEAT · CH1 BODY CAN", icon: "armchair", actions: [
{ name: "通风低档", command: "seat driver vent low" },
{ name: "通风高档", command: "seat driver vent high" },
{ name: "关闭通风", command: "seat driver vent off" },
{ name: "加热低档", command: "seat driver heat low" },
{ name: "加热高档", command: "seat driver heat high" },
{ name: "关闭加热", command: "seat driver heat off" },
{ name: "按摩弱档", command: "seat driver massage strength weak" },
{ name: "舒缓按摩", command: "seat driver massage mode relax" },
{ name: "关闭按摩", command: "seat driver massage off" },
] },
{ name: "空调", code: "CLIMATE · CAN", icon: "snowflake", actions: [
{ name: "开启空调", command: "打开空调" },
{ name: "24℃", command: "把空调温度调到24度" },
{ name: "风量 3", command: "把空调风量调到3档" },
{ name: "自动模式", command: "把空调设置为自动模式" },
] },
{ name: "电动出风口", code: "OUTLET · 4 ZONES", icon: "wind", actions: [
{ name: "主驾左", command: "把主驾左出风口调到中间位置" },
{ name: "主驾右", command: "把主驾右出风口调到中间位置" },
{ name: "副驾左", command: "把副驾左出风口调到中间位置" },
{ name: "副驾右", command: "把副驾右出风口调到中间位置" },
{ name: "开启空调", command: "ac on" },
{ name: "关闭空调", command: "ac off" },
{ name: "24℃", command: "ac temp driver 24" },
{ name: "风量 3", command: "ac fan 3" },
{ name: "除霜出风", command: "ac mode defrost" },
{ name: "自动模式", command: "ac control auto" },
] },
{ name: "遮阳帘", code: "ROOF · SHADE", icon: "panels-top-left", actions: [
{ name: "打开", command: "打开遮阳帘" },
{ name: "关闭", command: "关闭遮阳帘" },
{ name: "半开", command: "把遮阳帘打开一半" },
{ name: "停止", command: "停止遮阳帘" },
{ name: "开启 10%", command: "shade position 10" },
{ name: "打开", command: "shade position 100" },
{ name: "关闭", command: "shade position 0" },
{ name: "半开", command: "shade position 50" },
] },
{ name: "车内灯光", code: "INTERIOR · LIGHT", icon: "lamp-ceiling", actions: [
{ name: "室内灯", command: "打开车内室内灯" },
{ name: "阅读灯", command: "打开全部阅读灯" },
{ name: "氛围灯", command: "打开车内氛围灯" },
{ name: "氛围亮度", command: "把氛围灯亮度调到3档" },
{ name: "室内灯", command: "light indoor on" },
{ name: "关闭室内灯", command: "light indoor off" },
{ name: "阅读灯", command: "light reading all on" },
{ name: "关闭阅读灯", command: "light reading all off" },
{ name: "氛围灯", command: "light ambient on" },
{ name: "关闭氛围灯", command: "light ambient off" },
{ name: "氛围色号 32", command: "light ambient color 32" },
{ name: "氛围亮度", command: "light ambient brightness 3" },
] },
{ name: "车窗与门锁", code: "WINDOW · DOOR", icon: "gallery-vertical-end", actions: [
{ name: "全部开窗", command: "打开全部车窗" },
{ name: "全部关窗", command: "关闭全部车窗" },
{ name: "左前窗 50%", command: "把左前车窗打开到50%" },
{ name: "后排门锁", command: "锁上后排门锁" },
{ name: "车窗", code: "WINDOW · CH1", icon: "gallery-vertical-end", actions: [
{ name: "左前窗 10%", command: "window front-left position 10" },
{ name: "左前窗一半", command: "window front-left position 50" },
{ name: "左前窗关闭", command: "window front-left position 0" },
] },
];
@@ -199,6 +210,10 @@
let isPlanning = false;
let isExecuting = false;
let healthState = null;
let healthRefreshPromise = null;
let healthPollTimer = null;
let healthToastRequested = false;
let latestVehicleStateTurn = -1;
function uid(prefix) {
if (globalThis.crypto?.randomUUID) return `${prefix}-${crypto.randomUUID()}`;
@@ -402,6 +417,9 @@
<div class="v2-extension-vehicle-grid" id="extensionVehicleGrid"></div>
</section>
</div>
<div class="v2-extension-run-plan" id="extensionVehicleStateSummary" data-state="unknown">
<span><i>CH1</i>等待权威车端状态</span>
</div>
<div class="v2-extension-run-plan" id="extensionRunPlan" hidden></div>
</section>`;
moduleSwitcher.insertAdjacentElement("afterend", root);
@@ -489,11 +507,106 @@
}
const payload = await response.json().catch(() => ({}));
if (!response.ok || payload.success === false) {
throw new Error(payload.message || `自扩展请求失败(HTTP ${response.status})`);
const error = new Error(payload.message || `自扩展请求失败(HTTP ${response.status})`);
error.payload = payload;
throw error;
}
return payload;
}
function sharedConversationId() {
const fromConsole = window.CognitiveAgentConsole?.getConversationId?.();
if (typeof fromConsole === "string" && fromConsole.startsWith("conv-")) {
sessionStorage.setItem(CONVERSATION_KEY, fromConsole);
return fromConsole;
}
const stored = sessionStorage.getItem(CONVERSATION_KEY);
if (stored?.startsWith("conv-")) return stored;
const conversationId = uid("conv");
sessionStorage.setItem(CONVERSATION_KEY, conversationId);
return conversationId;
}
function containsAuthoritativePhysicalCh1State(value) {
if (!value || typeof value !== "object") return false;
if (value.source === "physical_can_ch1" || value.feedback_source === "physical_can_ch1") {
return true;
}
return Object.values(value).some((item) => containsAuthoritativePhysicalCh1State(item));
}
function isAuthoritativeVehicleStatePayload(payload) {
if (!payload || typeof payload !== "object") return false;
if (payload.conversation_id !== sharedConversationId()) return false;
return containsAuthoritativePhysicalCh1State(payload.actuator_state)
|| containsAuthoritativePhysicalCh1State(payload.domain_state);
}
function renderAuthoritativeVehicleState(payload) {
if (!isAuthoritativeVehicleStatePayload(payload)) return false;
const turn = Number(payload.conversation_turn);
if (Number.isFinite(turn) && turn < latestVehicleStateTurn) return false;
if (Number.isFinite(turn)) latestVehicleStateTurn = turn;
const summary = $("#extensionVehicleStateSummary");
if (!summary) return false;
const requestLabel = String(payload.request_id || "-").slice(-8);
const items = [
`<span class="done"><i>CH1</i>真实车况 · TURN ${Number.isFinite(turn) ? turn : "-"} · ${escapeHtml(requestLabel)}</span>`,
];
const feedback = Array.isArray(payload.physical_feedback)
? payload.physical_feedback.filter((item) => (
item?.confirmed === true
&& item?.channel === 1
&& item?.source === "physical_can_ch1"
))
: [];
if (feedback.length) {
const frames = [...new Set(feedback.map((item) => item.frame?.frame_id).filter(Boolean))];
items.push(`<span><i>FB</i>${feedback.length}/${payload.physical_feedback.length} MATCH · ${escapeHtml(frames.join(" / ") || "CH1")}</span>`);
}
const driver = payload.actuator_state?.driver || {};
const ventilation = driver.ventilation;
const heating = driver.heating;
if (ventilation?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>VENT</i>${ventilation.state === "on" ? `ON L${escapeHtml(ventilation.level)}` : "OFF"}</span>`);
}
if (heating?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>HEAT</i>${heating.state === "on" ? `ON L${escapeHtml(heating.level)}` : "OFF"}</span>`);
}
const massage = driver.massage;
if (massage?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>MASSAGE</i>${escapeHtml(massage.mode || "off")} · ${escapeHtml(massage.strength || "off")}</span>`);
}
const climate = payload.domain_state?.climate;
if (climate?.feedback_source === "physical_can_ch1") {
const temperature = climate.zones?.driver?.temperature_c ?? "-";
const outletMode = climate.zones?.driver?.outlet_mode || "-";
items.push(`<span><i>AC</i>${escapeHtml(climate.power || climate.ac || "unknown")} · ${escapeHtml(temperature)}℃ · FAN ${escapeHtml(climate.fan_level ?? "-")} · ${escapeHtml(outletMode)}</span>`);
}
const roof = payload.domain_state?.roof;
if (roof?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>ROOF</i>${escapeHtml(roof.shade_position_pct ?? "-")}% · ${escapeHtml(roof.motion_state || "unknown")}</span>`);
}
const frontLeftWindow = payload.domain_state?.window?.positions?.front_left;
if (frontLeftWindow?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>WINDOW</i>左前 ${escapeHtml(frontLeftWindow.position_pct ?? "-")}%</span>`);
}
const interiorLight = payload.domain_state?.interior_light;
if (interiorLight?.feedback_source === "physical_can_ch1") {
items.push(`<span><i>INTERIOR</i>${escapeHtml(interiorLight.power || "unknown")}</span>`);
}
const ambientLight = payload.domain_state?.ambient_light;
if (ambientLight?.feedback_source === "physical_can_ch1") {
const brightness = `${ambientLight.front_brightness_level ?? "-"}/${ambientLight.rear_brightness_level ?? "-"}`;
const color = `${ambientLight.front_color_index ?? "-"}/${ambientLight.rear_color_index ?? "-"}`;
items.push(`<span><i>AMBIENT</i>${escapeHtml(ambientLight.power || "unknown")} · L${escapeHtml(brightness)} · C${escapeHtml(color)}</span>`);
}
summary.dataset.state = "physical_can_ch1";
summary.innerHTML = items.join("");
return true;
}
function setMode(mode) {
activeMode = mode === "control" ? "control" : "study";
sessionStorage.setItem(MODE_KEY, activeMode);
@@ -864,20 +977,51 @@
}
function hydratePlan(steps) {
return (steps || []).map((step) => {
if (!Array.isArray(steps)) throw new Error("联动计划步骤格式无效。");
if (steps.length > MAX_PLAN_STEPS) throw new Error("联动计划步骤过多。");
const plan = steps.map((step) => {
if (!step || typeof step !== "object" || Array.isArray(step)) {
throw new Error("联动计划包含无效步骤。");
}
if (step.kind === "delay") {
if (!Number.isInteger(step.delay_ms)
|| step.delay_ms < MIN_PLAN_DELAY_MS
|| step.delay_ms > MAX_PLAN_DELAY_MS) {
throw new Error("延时步骤必须使用安全范围内的整数毫秒。");
}
if ([step.capability_id, step.command].some(
(value) => value != null && String(value).trim() !== "",
)) {
throw new Error("延时步骤不能包含设备命令。");
}
return {
kind: "delay",
delayMs: step.delay_ms,
label: step.delay_ms % 1000 === 0
? `等待 ${step.delay_ms / 1000} 秒`
: `等待 ${step.delay_ms} 毫秒`,
risk: "low",
};
}
if (step.delay_ms !== undefined && step.delay_ms !== 0) {
throw new Error("非延时步骤不能携带延时值。");
}
if (step.kind === "vehicle") {
const command = String(step.command || "").trim();
if (!command) throw new Error("车端计划步骤缺少命令。");
return {
kind: "vehicle",
command: step.command,
label: step.label || step.command,
command,
label: step.label || command,
risk: step.risk || "medium",
};
}
if (step.kind !== "peripheral") throw new Error("联动计划包含未知步骤。");
const capability = state.capabilities.find((item) => item.id === step.capability_id);
const device = state.devices.find((item) => item.id === capability?.deviceId);
if (!capability || !device || !learnedCapabilities(device.id).some(
(item) => item.id === capability.id,
)) return null;
)) throw new Error("联动计划引用了尚未学习的外设能力。");
return {
kind: "peripheral",
device,
@@ -885,7 +1029,22 @@
label: `${device.name} · ${capability.name}`,
risk: capability.risk || "low",
};
}).filter(Boolean);
});
let totalDelayMs = 0;
plan.forEach((step, index) => {
if (step.kind !== "delay") return;
if (index === 0 || index === plan.length - 1) {
throw new Error("延时步骤必须位于两个动作之间。");
}
if (plan[index - 1].kind === "delay") {
throw new Error("联动计划不能包含连续延时。");
}
totalDelayMs += step.delayMs;
});
if (totalDelayMs > MAX_TOTAL_PLAN_DELAY_MS) {
throw new Error("联动计划累计延时超过安全上限。");
}
return plan;
}
function setExecutionState(stateName, title, note, progress = 0) {
@@ -955,11 +1114,92 @@
}
}
async function executeVehicle(command) {
return api("vehicle/execute", {
method: "POST",
body: JSON.stringify({ command, vehicle_id: "denza_n7" }),
function vehicleExecutionSucceeded(result) {
const operations = Array.isArray(result?.operation_results)
? result.operation_results
: [];
const everyOperationSucceeded = operations.length > 0
&& operations.every((item) => item?.status === "succeeded");
const hardwareRequest = result?.execution_mode === "hardware";
const feedback = Array.isArray(result?.physical_feedback)
? result.physical_feedback
: [];
const everyHardwareOperationConfirmed = !hardwareRequest || (
result?.physical_feedback_confirmed === true
&& feedback.length === operations.length
&& feedback.every((item) => item?.confirmed === true)
);
return result?.success === true
&& result?.status === "succeeded"
&& everyOperationSucceeded
&& everyHardwareOperationConfirmed;
}
function synchronizeVehicleExecution(command, result) {
if (!result || typeof result !== "object") return;
const completed = vehicleExecutionSucceeded(result);
state.lastVehicleExecution = {
at: new Date().toISOString(),
command,
success: completed,
status: result.status || "unknown",
executionMode: result.execution_mode || null,
requestId: result.request_id || null,
operationResults: result.operation_results || [],
executionReport: result.execution_report || null,
domainState: result.domain_state || null,
actuatorState: result.actuator_state || null,
physicalState: result.physical_state || null,
physicalFeedback: result.physical_feedback || [],
physicalFeedbackConfirmed: result.physical_feedback_confirmed === true,
conversationId: result.conversation_id || null,
conversationTurn: result.conversation_turn ?? null,
};
saveState();
renderAuthoritativeVehicleState(result);
$$('[data-extension-vehicle-command]').forEach((button) => {
if (button.dataset.extensionVehicleCommand !== command) return;
button.dataset.executionState = completed ? "succeeded" : "failed";
button.setAttribute(
"aria-label",
`${button.textContent.trim()}:${completed ? "执行成功" : "执行未完成"}`,
);
button.title = `${command} · ${result.message || result.status || "执行结果未知"}`;
});
window.dispatchEvent(new CustomEvent("cognitive-agent:extension-vehicle-result", {
detail: result,
}));
}
function vehicleExecutionError(result) {
const failed = (result?.operation_results || []).find(
(item) => item?.status !== "succeeded",
);
return result?.message
|| failed?.message
|| `车端动作未成功完成(${result?.status || "unknown"})`;
}
async function executeVehicle(command) {
let result;
try {
result = await api("vehicle/execute", {
method: "POST",
// Channel, seat target, provider, vehicle profile and execution mode
// are intentionally absent: those are server-side CH1 safety policy.
body: JSON.stringify({ command, conversation_id: sharedConversationId() }),
});
} catch (error) {
if (error.payload) synchronizeVehicleExecution(command, error.payload);
throw error;
}
synchronizeVehicleExecution(command, result);
if (!vehicleExecutionSucceeded(result)) {
const error = new Error(vehicleExecutionError(result));
error.payload = result;
throw error;
}
return result;
}
async function executePlan(plan) {
@@ -968,6 +1208,7 @@
renderRunPlan(plan);
let completed = 0;
let vehicleDryRun = false;
let vehicleHardwareFeedback = 0;
for (let index = 0; index < plan.length; index += 1) {
const step = plan[index];
renderRunPlan(plan, index);
@@ -978,9 +1219,14 @@
(index / plan.length) * 100,
);
try {
if (step.kind === "vehicle") {
if (step.kind === "delay") {
await delay(step.delayMs);
} else if (step.kind === "vehicle") {
const result = await executeVehicle(step.command);
vehicleDryRun ||= result.execution_mode === "dry_run";
if (result.execution_mode === "hardware") {
vehicleHardwareFeedback += result.physical_feedback.length;
}
} else {
await sendGatewayCommand("emit", { slot: step.capability.slot });
}
@@ -992,14 +1238,21 @@
showToast(error.message, "error");
break;
}
if (index < plan.length - 1) await delay(state.settings.actionDelayMs);
const nextStep = plan[index + 1];
if (nextStep && step.kind !== "delay" && nextStep.kind !== "delay") {
await delay(state.settings.actionDelayMs);
}
}
renderRunPlan(plan, completed);
if (completed === plan.length) {
setExecutionState(
"success",
"联动计划已全部处理",
vehicleDryRun ? "外设已发射,车端处于安全演练模式" : "外设与车端链路处理完成",
vehicleDryRun
? "外设已发射,车端处于安全演练模式"
: vehicleHardwareFeedback > 0
? `车端 ${vehicleHardwareFeedback} 项动作均已同步物理反馈`
: "外设与车端链路处理完成",
100,
);
showToast("联动计划已全部处理。", "success");
@@ -1038,46 +1291,97 @@
}]);
}
async function refreshHealth(showMessage = false) {
$("#extensionStudyHealth").dataset.state = "checking";
$("#extensionStudyHealthState").textContent = "CHECK";
$("#extensionVehicleCard").dataset.state = "checking";
$("#extensionVehicleState").textContent = "CHECK";
async function requestHealth() {
const controller = new AbortController();
const timeoutId = window.setTimeout(() => controller.abort(), HEALTH_REQUEST_TIMEOUT_MS);
try {
healthState = await api("health");
} catch (error) {
healthState = {
connected: false,
transport: "unavailable",
message: error.message,
gpt: { configured: false },
vehicle: { connected: false },
};
return await api("health", { signal: controller.signal });
} finally {
window.clearTimeout(timeoutId);
}
const connected = Boolean(healthState.connected);
const transport = String(healthState.transport || "--").toUpperCase();
const connectionLabel = connected
? (healthState.transport === "simulation" ? "模拟学习器已就绪" : `${transport} 学习器已连接`)
: "GSCU1B 尚未连接";
$("#extensionStudyHealthText").textContent = connectionLabel;
$("#extensionStudyHealthState").textContent = connected ? "READY" : "OFFLINE";
$("#extensionStudyHealth").dataset.state = connected ? "online" : "offline";
const vehicleConnected = Boolean(healthState.vehicle?.connected);
$("#extensionVehicleState").textContent = vehicleConnected ? "READY" : "OFFLINE";
$("#extensionVehicleCard").dataset.state = vehicleConnected ? "online" : "offline";
$("#extensionConnectionText").textContent = connectionLabel;
$(".v2-extension-footer").classList.toggle("offline", !connected);
$("#extensionModelName").textContent = healthState.gpt?.configured
? healthState.gpt.model || "GPT"
: "GPT 未配置";
$("#extensionModelState").textContent = healthState.gpt?.configured ? "READY" : "LOCAL";
$("#extensionModelCard").classList.toggle("unavailable", !healthState.gpt?.configured);
if (showMessage) {
const ready = [connected, healthState.gpt?.configured, healthState.vehicle?.connected]
.filter(Boolean).length;
showToast(`自扩展链路:${ready} / 3 项就绪。`, ready === 3 ? "success" : "error");
}
return healthState;
}
function refreshHealth(showMessage = false) {
healthToastRequested ||= showMessage;
if (healthRefreshPromise) return healthRefreshPromise;
healthRefreshPromise = (async () => {
if (!healthState || healthToastRequested) {
$("#extensionStudyHealth").dataset.state = "checking";
$("#extensionStudyHealthState").textContent = "CHECK";
$("#extensionVehicleCard").dataset.state = "checking";
$("#extensionVehicleState").textContent = "CHECK";
}
try {
healthState = await requestHealth();
} catch (error) {
healthState = {
connected: false,
listening: false,
transport: "unavailable",
message: error.message,
gpt: { configured: false },
vehicle: { connected: false },
};
}
const connected = Boolean(healthState.connected);
const listening = !connected && Boolean(healthState.listening);
const transport = String(healthState.transport || "--").toUpperCase();
let connectionLabel;
if (connected) {
const peer = String(healthState.peer || "").trim();
connectionLabel = healthState.transport === "simulation"
? "模拟学习器已就绪"
: `${transport} 学习器已连接${peer ? ` · ${peer}` : ""}`;
} else if (listening) {
connectionLabel = `${transport} 监听已就绪,等待 GSCU1B 接入`;
} else {
connectionLabel = healthState.message || "GSCU1B 尚未连接";
}
const studyHealthText = $("#extensionStudyHealthText");
studyHealthText.textContent = connectionLabel;
studyHealthText.title = healthState.message || connectionLabel;
$("#extensionStudyHealthState").textContent = connected
? "READY"
: (listening ? "WAIT" : "OFFLINE");
$("#extensionStudyHealth").dataset.state = connected
? "online"
: (listening ? "checking" : "offline");
const vehicleConnected = Boolean(healthState.vehicle?.connected);
$("#extensionVehicleState").textContent = vehicleConnected ? "READY" : "OFFLINE";
$("#extensionVehicleCard").dataset.state = vehicleConnected ? "online" : "offline";
const connectionText = $("#extensionConnectionText");
connectionText.textContent = connectionLabel;
connectionText.title = healthState.message || connectionLabel;
$(".v2-extension-footer").classList.toggle("offline", !connected);
$("#extensionModelName").textContent = healthState.gpt?.configured
? healthState.gpt.model || "GPT"
: "GPT 未配置";
$("#extensionModelState").textContent = healthState.gpt?.configured ? "READY" : "LOCAL";
$("#extensionModelCard").classList.toggle("unavailable", !healthState.gpt?.configured);
if (healthToastRequested) {
const ready = [connected, healthState.gpt?.configured, healthState.vehicle?.connected]
.filter(Boolean).length;
showToast(`自扩展链路:${ready} / 3 项就绪。`, ready === 3 ? "success" : "error");
}
healthToastRequested = false;
return healthState;
})().finally(() => {
healthRefreshPromise = null;
});
return healthRefreshPromise;
}
function startHealthPolling() {
if (healthPollTimer !== null) return;
healthPollTimer = window.setInterval(() => {
if (document.visibilityState === "visible"
&& document.body.dataset.module === "self-extension") refreshHealth();
}, HEALTH_POLL_INTERVAL_MS);
}
function stopHealthPolling() {
if (healthPollTimer !== null) window.clearInterval(healthPollTimer);
healthPollTimer = null;
}
async function resetSession() {
@@ -1208,6 +1512,19 @@
refreshHealth();
}
});
window.addEventListener("cognitive-agent:result", (event) => {
renderAuthoritativeVehicleState(event.detail);
});
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible"
&& document.body.dataset.module === "self-extension") refreshHealth();
});
window.addEventListener("pagehide", stopHealthPolling);
window.addEventListener("pageshow", () => {
startHealthPolling();
if (document.visibilityState === "visible"
&& document.body.dataset.module === "self-extension") refreshHealth();
});
}
function init() {
@@ -1216,6 +1533,7 @@
renderAll();
setMode(activeMode);
if (document.body.dataset.module === "self-extension") refreshHealth();
startHealthPolling();
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}