mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-10 03:13:20 +08:00
Show key-alarm WSS status on the Netx Ops Plugins card (0.1.16).
Host publishes connection phases over Connection RPC; the card polls and badges Connected / Reconnecting / Auth failed so operators need not curl logs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
12f7018103
commit
e8b7d7331f
14 changed files with 783 additions and 55 deletions
125
lib/index.js
125
lib/index.js
|
|
@ -10,6 +10,46 @@ import z from "@deepseek-ai/schemastery";
|
|||
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
||||
import * as DshSettings from "@deepseek-ai/dsh-settings";
|
||||
|
||||
// src/netx/alarm-push-status.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.alarm-push-status");
|
||||
function emptyStatus(partial = {}) {
|
||||
return {
|
||||
phase: "disabled",
|
||||
enabled: false,
|
||||
wsUrl: "",
|
||||
detail: "",
|
||||
updatedAt: Date.now(),
|
||||
lastConnectedAt: null,
|
||||
lastError: null,
|
||||
...partial
|
||||
};
|
||||
}
|
||||
function store() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY];
|
||||
if (current === undefined) {
|
||||
current = { status: emptyStatus(), listeners: new Set };
|
||||
root[STORE_KEY] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getAlarmPushStatus() {
|
||||
return { ...store().status };
|
||||
}
|
||||
function publishAlarmPushStatus(next) {
|
||||
const state = store();
|
||||
state.status = {
|
||||
...state.status,
|
||||
...next,
|
||||
updatedAt: Date.now()
|
||||
};
|
||||
for (const listener of state.listeners)
|
||||
listener();
|
||||
}
|
||||
function resetAlarmPushStatus() {
|
||||
publishAlarmPushStatus(emptyStatus({ phase: "disabled", enabled: false }));
|
||||
}
|
||||
|
||||
// src/netx/alarm-push.ts
|
||||
function alarmSubscribeUrl(apiUrl) {
|
||||
const trimmed = apiUrl.trim().replace(/\/$/, "");
|
||||
|
|
@ -76,17 +116,35 @@ function formatAlarmPrompt(payload, lang = "zh") {
|
|||
].join(`
|
||||
`);
|
||||
}
|
||||
function setPhase(phase, wsUrl, extra = {}) {
|
||||
publishAlarmPushStatus({
|
||||
phase,
|
||||
enabled: true,
|
||||
wsUrl,
|
||||
detail: extra.detail ?? "",
|
||||
lastError: extra.lastError === undefined ? null : extra.lastError,
|
||||
...extra.lastConnectedAt !== undefined ? { lastConnectedAt: extra.lastConnectedAt } : {}
|
||||
});
|
||||
}
|
||||
function startAlarmPushClient(options) {
|
||||
const log = options.logger ?? console;
|
||||
const wsUrl = alarmSubscribeUrl(options.apiUrl);
|
||||
const token = options.token.trim();
|
||||
if (!wsUrl || !token) {
|
||||
log.warn?.("netxops alarm-push: missing apiUrl or token — not connecting");
|
||||
setPhase("error", wsUrl, {
|
||||
detail: "missing_url_or_token",
|
||||
lastError: "missing apiUrl or token"
|
||||
});
|
||||
return () => {};
|
||||
}
|
||||
const WS = options.WebSocketImpl ?? globalThis.WebSocket;
|
||||
if (typeof WS !== "function") {
|
||||
log.error?.("netxops alarm-push: WebSocket is unavailable in this runtime");
|
||||
setPhase("error", wsUrl, {
|
||||
detail: "websocket_unavailable",
|
||||
lastError: "WebSocket unavailable"
|
||||
});
|
||||
return () => {};
|
||||
}
|
||||
let closed = false;
|
||||
|
|
@ -100,12 +158,16 @@ function startAlarmPushClient(options) {
|
|||
reconnectTimer = undefined;
|
||||
}
|
||||
};
|
||||
const scheduleReconnect = () => {
|
||||
const scheduleReconnect = (reason) => {
|
||||
if (closed)
|
||||
return;
|
||||
clearReconnect();
|
||||
const delay = Math.min(60000, baseDelay * 2 ** Math.min(attempt, 5));
|
||||
attempt += 1;
|
||||
setPhase("reconnecting", wsUrl, {
|
||||
detail: `retry_in_${delay}ms`,
|
||||
lastError: reason
|
||||
});
|
||||
reconnectTimer = setTimeout(() => {
|
||||
connect();
|
||||
}, delay);
|
||||
|
|
@ -114,15 +176,17 @@ function startAlarmPushClient(options) {
|
|||
if (closed)
|
||||
return;
|
||||
clearReconnect();
|
||||
setPhase(attempt > 0 ? "reconnecting" : "connecting", wsUrl, { detail: "dialing" });
|
||||
try {
|
||||
socket = new WS(wsUrl);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.warn?.("netxops alarm-push: connect failed:", error);
|
||||
scheduleReconnect();
|
||||
scheduleReconnect(message);
|
||||
return;
|
||||
}
|
||||
socket.addEventListener("open", () => {
|
||||
attempt = 0;
|
||||
setPhase("authenticating", wsUrl, { detail: "auth" });
|
||||
socket?.send(JSON.stringify({ type: "auth", token }));
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
|
|
@ -134,11 +198,20 @@ function startAlarmPushClient(options) {
|
|||
}
|
||||
const type = String(msg.type ?? "").toLowerCase();
|
||||
if (type === "auth-ok") {
|
||||
attempt = 0;
|
||||
const now = Date.now();
|
||||
setPhase("connected", wsUrl, {
|
||||
detail: String(msg.user ?? "ok"),
|
||||
lastError: null,
|
||||
lastConnectedAt: now
|
||||
});
|
||||
log.info?.("netxops alarm-push: subscribed to %s", wsUrl);
|
||||
return;
|
||||
}
|
||||
if (type === "auth-fail") {
|
||||
log.error?.("netxops alarm-push: auth failed (%s)", msg.error);
|
||||
const err = String(msg.error ?? "auth_failed");
|
||||
log.error?.("netxops alarm-push: auth failed (%s)", err);
|
||||
setPhase("auth_failed", wsUrl, { detail: err, lastError: err });
|
||||
socket?.close();
|
||||
return;
|
||||
}
|
||||
|
|
@ -154,7 +227,7 @@ function startAlarmPushClient(options) {
|
|||
socket.addEventListener("close", () => {
|
||||
socket = null;
|
||||
if (!closed)
|
||||
scheduleReconnect();
|
||||
scheduleReconnect("socket_closed");
|
||||
});
|
||||
socket.addEventListener("error", () => {});
|
||||
};
|
||||
|
|
@ -297,18 +370,18 @@ function resetAlarmSession() {
|
|||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.connection-store");
|
||||
function store() {
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY];
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY] = current;
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function publishNetxConnection(next) {
|
||||
const state = store();
|
||||
const state = store2();
|
||||
state.connection = next;
|
||||
for (const listener of state.listeners)
|
||||
listener();
|
||||
|
|
@ -317,6 +390,7 @@ function publishNetxConnection(next) {
|
|||
// src/index.ts
|
||||
var name = "netxops";
|
||||
var inject = ["credentials"];
|
||||
var NETXOPS_RPC_CHANNEL = "/netxops";
|
||||
var NETXOPS_SETTINGS_NAMESPACE = "netxops";
|
||||
var NETXOPS_PRESET_ID = "netxops";
|
||||
var DEFAULT_TOKEN_REF = "NETX_API_TOKEN";
|
||||
|
|
@ -385,10 +459,19 @@ function apply(ctx, config = Config({})) {
|
|||
stopAlarmPush?.();
|
||||
stopAlarmPush = undefined;
|
||||
resetAlarmSession();
|
||||
if (!enabled)
|
||||
if (!enabled) {
|
||||
resetAlarmPushStatus();
|
||||
return;
|
||||
}
|
||||
if (!token.trim()) {
|
||||
ctx.logger.warn("netxops alarm-push: enabled but token is empty — not connecting");
|
||||
publishAlarmPushStatus({
|
||||
phase: "error",
|
||||
enabled: true,
|
||||
wsUrl: "",
|
||||
detail: "missing_token",
|
||||
lastError: "token empty"
|
||||
});
|
||||
return;
|
||||
}
|
||||
stopAlarmPush = startAlarmPushClient({
|
||||
|
|
@ -436,11 +519,30 @@ function apply(ctx, config = Config({})) {
|
|||
if (String(ref) === source().tokenCredentialRef)
|
||||
publish();
|
||||
});
|
||||
ctx.inject(["connection"], (connCtx) => {
|
||||
const rpc = connCtx.connection?.rpc;
|
||||
if (!rpc || typeof rpc.handle !== "function") {
|
||||
connCtx.logger.warn("netxops: connection.rpc.handle unavailable — alarm status UI disabled");
|
||||
return;
|
||||
}
|
||||
connCtx.effect(() => {
|
||||
const dispose = rpc.handle(NETXOPS_RPC_CHANNEL, async (endpoint) => {
|
||||
if (endpoint !== "alarm-push.status") {
|
||||
return { ok: false, error: { code: "bad-request", message: "Unknown endpoint." } };
|
||||
}
|
||||
return { ok: true, value: getAlarmPushStatus() };
|
||||
});
|
||||
return () => {
|
||||
dispose();
|
||||
};
|
||||
}, "netxops: alarm-push status rpc");
|
||||
});
|
||||
ctx.effect(() => () => {
|
||||
generation += 1;
|
||||
stopAlarmPush?.();
|
||||
stopAlarmPush = undefined;
|
||||
resetAlarmSession();
|
||||
resetAlarmPushStatus();
|
||||
}, "netxops: dispose host bridge");
|
||||
}
|
||||
export {
|
||||
|
|
@ -449,6 +551,7 @@ export {
|
|||
ensureAgentPresetInstalled,
|
||||
apply,
|
||||
NETXOPS_SETTINGS_NAMESPACE,
|
||||
NETXOPS_RPC_CHANNEL,
|
||||
NETXOPS_PRESET_ID,
|
||||
DEFAULT_TOKEN_REF,
|
||||
Config
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue