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:
oliver 2026-09-05 21:08:14 +08:00
parent 12f7018103
commit e8b7d7331f
14 changed files with 783 additions and 55 deletions

View file

@ -53,6 +53,65 @@ module.exports = __toCommonJS(exports_client);
// src/client/NetxopsCard.tsx
var import_react = require("react");
// src/client/alarm-push-status-view.ts
var NETXOPS_RPC_CHANNEL = "/netxops";
var ALARM_PUSH_STATUS_ENDPOINT = "alarm-push.status";
var EMPTY = {
phase: "disabled",
enabled: false,
wsUrl: "",
detail: "",
updatedAt: 0,
lastConnectedAt: null,
lastError: null
};
function asAlarmPushStatus(value) {
if (value === null || typeof value !== "object" || Array.isArray(value))
return { ...EMPTY };
const row = value;
const phase = typeof row.phase === "string" ? row.phase : "disabled";
return {
phase,
enabled: row.enabled === true,
wsUrl: typeof row.wsUrl === "string" ? row.wsUrl : "",
detail: typeof row.detail === "string" ? row.detail : "",
updatedAt: typeof row.updatedAt === "number" ? row.updatedAt : 0,
lastConnectedAt: typeof row.lastConnectedAt === "number" ? row.lastConnectedAt : null,
lastError: typeof row.lastError === "string" ? row.lastError : null
};
}
async function fetchAlarmPushStatus(call, signal) {
const result = await call(NETXOPS_RPC_CHANNEL, ALARM_PUSH_STATUS_ENDPOINT, {}, signal);
if (result !== null && typeof result === "object" && result.ok === true) {
return asAlarmPushStatus(result.value);
}
if (result !== null && typeof result === "object" && result.ok === false) {
return {
...EMPTY,
phase: "error",
enabled: true,
detail: "rpc_error",
lastError: String(result.error?.message ?? "rpc failed")
};
}
return asAlarmPushStatus(result);
}
function alarmPushTone(phase) {
switch (phase) {
case "connected":
return "ok";
case "connecting":
case "authenticating":
case "reconnecting":
return "warn";
case "auth_failed":
case "error":
return "err";
default:
return "mute";
}
}
// src/client/styles.ts
var STYLE_ID = "dsh-netxops-card-css";
var CSS = `
@ -65,6 +124,10 @@ var CSS = `
.dsh-nx-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
.dsh-nx-desc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
.dsh-nx-pending{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
.dsh-nx-status{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-tertiary)}
.dsh-nx-statusOk{color:var(--dsw-alias-label-primary);background:color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent)}
.dsh-nx-statusWarn{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform)}
.dsh-nx-statusErr{color:var(--dsw-alias-label-error);background:color-mix(in srgb, var(--dsw-alias-label-error) 12%, transparent)}
.dsh-nx-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s}
.dsh-nx-chevronOpen{transform:rotate(180deg)}
.dsh-nx-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
@ -106,6 +169,36 @@ function ensureStyles() {
// src/client/NetxopsCard.tsx
var jsx_runtime = require("react/jsx-runtime");
function phaseLocaleKey(phase) {
switch (phase) {
case "disabled":
return "alarmPushPhaseDisabled";
case "idle":
return "alarmPushPhaseIdle";
case "connecting":
return "alarmPushPhaseConnecting";
case "authenticating":
return "alarmPushPhaseAuthenticating";
case "connected":
return "alarmPushPhaseConnected";
case "reconnecting":
return "alarmPushPhaseReconnecting";
case "auth_failed":
return "alarmPushPhaseAuthFailed";
case "error":
return "alarmPushPhaseError";
default:
return "alarmPushPhaseDisabled";
}
}
function StatusBadge(props) {
const tone = alarmPushTone(props.phase);
const className = tone === "ok" ? "dsh-nx-status dsh-nx-statusOk" : tone === "warn" ? "dsh-nx-status dsh-nx-statusWarn" : tone === "err" ? "dsh-nx-status dsh-nx-statusErr" : "dsh-nx-status";
return /* @__PURE__ */ jsx_runtime.jsx("span", {
className,
children: props.label
});
}
function ValueField(props) {
return /* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-field",
@ -176,6 +269,8 @@ function NetxopsCard(props) {
const title = t("title");
const disabled = !state.writable;
const blocked = !state.dirty || state.invalid || state.saving;
const pushStatus = state.alarmPushStatus;
const showHeaderStatus = pushStatus !== null && (pushStatus.enabled || pushStatus.phase !== "disabled");
return /* @__PURE__ */ jsx_runtime.jsxs("li", {
className: open ? "dsh-nx-card dsh-nx-cardOpen" : "dsh-nx-card",
children: [
@ -201,6 +296,10 @@ function NetxopsCard(props) {
})
]
}),
showHeaderStatus && pushStatus ? /* @__PURE__ */ jsx_runtime.jsx(StatusBadge, {
phase: pushStatus.phase,
label: t(phaseLocaleKey(pushStatus.phase))
}) : null,
state.dirty ? /* @__PURE__ */ jsx_runtime.jsx("span", {
className: "dsh-nx-pending",
children: t("unsaved")
@ -300,24 +399,32 @@ function NetxopsCard(props) {
htmlFor: "netxops-alarm-push",
children: t("alarmPushEnabled")
}),
state.alarmPushEnabled.overridden ? /* @__PURE__ */ jsx_runtime.jsxs("span", {
/* @__PURE__ */ jsx_runtime.jsxs("span", {
className: "dsh-nx-badges",
children: [
/* @__PURE__ */ jsx_runtime.jsx("span", {
className: "dsh-nx-badge",
children: t("overridden")
}),
/* @__PURE__ */ jsx_runtime.jsx("button", {
type: "button",
className: "dsh-nx-reset",
disabled,
onClick: () => {
props.resetField("alarmPushEnabled");
},
children: t("reset")
})
pushStatus ? /* @__PURE__ */ jsx_runtime.jsx(StatusBadge, {
phase: pushStatus.phase,
label: `${t("alarmPushStatus")}: ${t(phaseLocaleKey(pushStatus.phase))}`
}) : null,
state.alarmPushEnabled.overridden ? /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, {
children: [
/* @__PURE__ */ jsx_runtime.jsx("span", {
className: "dsh-nx-badge",
children: t("overridden")
}),
/* @__PURE__ */ jsx_runtime.jsx("button", {
type: "button",
className: "dsh-nx-reset",
disabled,
onClick: () => {
props.resetField("alarmPushEnabled");
},
children: t("reset")
})
]
}) : null
]
}) : null
})
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("label", {
@ -337,7 +444,16 @@ function NetxopsCard(props) {
children: t("alarmPushEnabledHint")
})
]
})
}),
pushStatus?.wsUrl ? /* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-hint",
children: pushStatus.wsUrl
}) : null,
pushStatus?.lastError ? /* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-invalid",
role: "status",
children: pushStatus.lastError
}) : null
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("div", {
@ -580,6 +696,7 @@ class CardForm {
var NETXOPS_NS = "netxops";
var DEFAULT_TOKEN_REF = "NETX_API_TOKEN";
var API_TOKEN_FIELD = "apiToken";
var STATUS_POLL_MS = 2000;
class NetxopsCardController {
scope;
@ -592,6 +709,10 @@ class NetxopsCardController {
writable: false,
remoteReady: false
};
rpcCall;
alarmPushStatus = null;
pollTimer;
pollInFlight = false;
constructor(scope, ctx) {
this.scope = scope;
this.ctx = ctx;
@ -614,6 +735,48 @@ class NetxopsCardController {
if (ready)
this.readCredential();
}
setAlarmPushRpc(call) {
this.rpcCall = call;
if (call === undefined) {
this.stopStatusPoll();
if (this.alarmPushStatus !== null) {
this.alarmPushStatus = null;
this.store.set(this.projection());
}
return;
}
this.startStatusPoll();
this.refreshAlarmPushStatus();
}
startStatusPoll() {
if (this.pollTimer !== undefined)
return;
this.pollTimer = setInterval(() => {
this.refreshAlarmPushStatus();
}, STATUS_POLL_MS);
}
stopStatusPoll() {
if (this.pollTimer === undefined)
return;
clearInterval(this.pollTimer);
this.pollTimer = undefined;
}
async refreshAlarmPushStatus() {
const call = this.rpcCall;
if (call === undefined || this.pollInFlight)
return;
this.pollInFlight = true;
try {
const next = await fetchAlarmPushStatus(call);
const prev = this.alarmPushStatus;
if (prev && prev.phase === next.phase && prev.enabled === next.enabled && prev.wsUrl === next.wsUrl && prev.detail === next.detail && prev.lastError === next.lastError && prev.lastConnectedAt === next.lastConnectedAt)
return;
this.alarmPushStatus = next;
this.store.set(this.projection());
} catch {} finally {
this.pollInFlight = false;
}
}
projection() {
return {
...this.form.shell(),
@ -623,7 +786,8 @@ class NetxopsCardController {
apiToken: this.form.field(API_TOKEN_FIELD),
apiTokenConfigured: this.credential.configured,
apiTokenWritable: this.credential.remoteReady && this.credential.writable,
apiTokenRemoteReady: this.credential.remoteReady
apiTokenRemoteReady: this.credential.remoteReady,
alarmPushStatus: this.alarmPushStatus
};
}
credentials() {
@ -695,6 +859,15 @@ var en = {
langHint: "Response language hint (zh / en).",
alarmPushEnabled: "Key-alarm push",
alarmPushEnabledHint: "Dial out to netx and open/follow a DSH session when a matched key alarm arrives. WhatsApp/im is optional and not required.",
alarmPushStatus: "Push link",
alarmPushPhaseDisabled: "Off",
alarmPushPhaseIdle: "Idle",
alarmPushPhaseConnecting: "Connecting…",
alarmPushPhaseAuthenticating: "Authenticating…",
alarmPushPhaseConnected: "Connected",
alarmPushPhaseReconnecting: "Reconnecting…",
alarmPushPhaseAuthFailed: "Auth failed",
alarmPushPhaseError: "Error",
apiToken: "API token",
apiTokenHint: "Stored as credential NETX_API_TOKEN (never written into settings). Leave blank to keep the current token.",
apiTokenUnavailable: "This DSH build does not expose remote.credentials. Set the token with scripts/set-netx-token.ps1 (or .sh), then restart is not required if credentials are watched.",
@ -721,6 +894,15 @@ var zh = {
langHint: "响应语言提示(zh / en)。",
alarmPushEnabled: "关键告警推送",
alarmPushEnabledHint: "主动连接 netx;匹配到关键告警后在本机打开/续写一条 DSH 会话。不必安装 WhatsApp / im。",
alarmPushStatus: "推送链路",
alarmPushPhaseDisabled: "未开启",
alarmPushPhaseIdle: "空闲",
alarmPushPhaseConnecting: "连接中…",
alarmPushPhaseAuthenticating: "鉴权中…",
alarmPushPhaseConnected: "已连接",
alarmPushPhaseReconnecting: "重连中…",
alarmPushPhaseAuthFailed: "鉴权失败",
alarmPushPhaseError: "异常",
apiToken: "API Token",
apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。",
apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。",
@ -762,6 +944,17 @@ function apply(ctx) {
};
}, "netxops: credential invalidations");
});
ctx.inject(["connection"], (connCtx) => {
const call = connCtx.connection?.rpc?.call?.bind(connCtx.connection.rpc);
if (typeof call !== "function") {
connCtx.logger.warn("netxops: connection.rpc.call unavailable — alarm status UI disabled");
return;
}
card.setAlarmPushRpc(call);
connCtx.effect(() => () => {
card.setAlarmPushRpc(undefined);
}, "netxops: clear alarm-push rpc");
});
ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
name: "settings.plugin.item",
key: NETXOPS_NS,

View file

@ -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