feat: harden seat intent and expand HIL coverage
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
(function initializeCognitiveAgentNetwork() {
|
||||
const nativeFetch = window.fetch.bind(window);
|
||||
const listeners = new Set();
|
||||
const state = {
|
||||
status: navigator.onLine === false ? "offline" : "connecting",
|
||||
lastSuccessAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
let probeTimer = null;
|
||||
|
||||
function snapshot() {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function renderBadge() {
|
||||
const badge = document.querySelector("#connectionBadge");
|
||||
if (!badge) return;
|
||||
const presentation = {
|
||||
connecting: ["neutral", "loader-circle", "连接中"],
|
||||
online: ["", "wifi", "服务已连接"],
|
||||
degraded: ["warning", "wifi-low", "连接不稳定"],
|
||||
offline: ["danger", "wifi-off", "服务已断开"],
|
||||
}[state.status];
|
||||
badge.className = `status-badge connection-badge ${presentation[0]}`.trim();
|
||||
badge.innerHTML = `<i data-lucide="${presentation[1]}"></i><span>${presentation[2]}</span>`;
|
||||
badge.title = state.lastError || presentation[2];
|
||||
window.lucide?.createIcons?.({
|
||||
attrs: { "aria-hidden": "true" },
|
||||
nodes: [badge],
|
||||
});
|
||||
}
|
||||
|
||||
function publish(status, error = null) {
|
||||
const changed = state.status !== status || state.lastError !== error;
|
||||
state.status = status;
|
||||
state.lastError = error;
|
||||
if (status === "online") state.lastSuccessAt = new Date().toISOString();
|
||||
renderBadge();
|
||||
if (!changed) return;
|
||||
const detail = snapshot();
|
||||
listeners.forEach((listener) => listener(detail));
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:connection", { detail }));
|
||||
}
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
async function request(input, options = {}) {
|
||||
const {
|
||||
timeoutMs = 10000,
|
||||
retries: configuredRetries,
|
||||
signal: callerSignal,
|
||||
...fetchOptions
|
||||
} = options;
|
||||
const method = String(fetchOptions.method || "GET").toUpperCase();
|
||||
const retries = configuredRetries ?? (method === "GET" ? 1 : 0);
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
||||
const controller = new AbortController();
|
||||
const abortFromCaller = () => controller.abort(callerSignal?.reason);
|
||||
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
||||
const timeout = window.setTimeout(() => controller.abort("timeout"), timeoutMs);
|
||||
try {
|
||||
const response = await nativeFetch(input, {
|
||||
...fetchOptions,
|
||||
signal: controller.signal,
|
||||
});
|
||||
publish(response.status >= 500 ? "degraded" : "online",
|
||||
response.status >= 500 ? `服务返回 HTTP ${response.status}` : null);
|
||||
return response;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const message = controller.signal.aborted && !callerSignal?.aborted
|
||||
? `请求超时(${Math.round(timeoutMs / 1000)} 秒)`
|
||||
: (error?.message || "网络请求失败");
|
||||
publish(attempt < retries ? "degraded" : "offline", message);
|
||||
if (callerSignal?.aborted || attempt >= retries || method !== "GET") {
|
||||
throw new Error(message, { cause: error });
|
||||
}
|
||||
await sleep(300 * (attempt + 1));
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
callerSignal?.removeEventListener("abort", abortFromCaller);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function probe() {
|
||||
if (document.hidden) return;
|
||||
if (navigator.onLine === false) {
|
||||
publish("offline", "设备当前处于离线状态");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await request("/api/health", {
|
||||
cache: "no-store",
|
||||
timeoutMs: 4000,
|
||||
retries: 0,
|
||||
});
|
||||
if (!response.ok && response.status !== 401) {
|
||||
publish("degraded", `健康检查返回 HTTP ${response.status}`);
|
||||
}
|
||||
} catch (_error) {
|
||||
// request() already published the detailed state.
|
||||
}
|
||||
}
|
||||
|
||||
function startMonitoring() {
|
||||
if (probeTimer !== null) return;
|
||||
renderBadge();
|
||||
probe();
|
||||
probeTimer = window.setInterval(probe, 10000);
|
||||
window.addEventListener("online", () => {
|
||||
publish("connecting");
|
||||
probe();
|
||||
});
|
||||
window.addEventListener("offline", () => publish("offline", "设备当前处于离线状态"));
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) probe();
|
||||
});
|
||||
}
|
||||
|
||||
window.CognitiveAgentNetwork = {
|
||||
request,
|
||||
probe,
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
listener(snapshot());
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
getState: snapshot,
|
||||
nativeFetch,
|
||||
};
|
||||
document.addEventListener("DOMContentLoaded", startMonitoring, { once: true });
|
||||
})();
|
||||
Reference in New Issue
Block a user