139 lines
4.6 KiB
JavaScript
139 lines
4.6 KiB
JavaScript
(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 });
|
|
})();
|