Add optional IM sink for key alarms (0.1.17).

Settings choose DSH session and/or WhatsApp via botId+targetId; onAlarm soft-calls ctx.dshIm.send when im is installed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-05 21:36:50 +08:00
parent e8b7d7331f
commit 83c3796fbb
12 changed files with 482 additions and 23 deletions

View file

@ -456,6 +456,140 @@ function NetxopsCard(props) {
}) : null
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-field",
children: [
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-fieldHead",
children: [
/* @__PURE__ */ jsx_runtime.jsx("label", {
className: "dsh-nx-label",
htmlFor: "netxops-alarm-dsh",
children: t("alarmDeliverDsh")
}),
state.alarmDeliverDsh.overridden ? /* @__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("alarmDeliverDsh");
},
children: t("reset")
})
]
}) : null
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("label", {
className: "dsh-nx-checkRow",
htmlFor: "netxops-alarm-dsh",
children: [
/* @__PURE__ */ jsx_runtime.jsx("input", {
id: "netxops-alarm-dsh",
type: "checkbox",
checked: state.alarmDeliverDsh.text === "true",
disabled,
onChange: (event) => {
props.edit("alarmDeliverDsh", event.target.checked ? "true" : "false");
}
}),
/* @__PURE__ */ jsx_runtime.jsx("span", {
children: t("alarmDeliverDshHint")
})
]
})
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-field",
children: [
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-fieldHead",
children: [
/* @__PURE__ */ jsx_runtime.jsx("label", {
className: "dsh-nx-label",
htmlFor: "netxops-alarm-im",
children: t("alarmDeliverIm")
}),
state.alarmDeliverIm.overridden ? /* @__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("alarmDeliverIm");
},
children: t("reset")
})
]
}) : null
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("label", {
className: "dsh-nx-checkRow",
htmlFor: "netxops-alarm-im",
children: [
/* @__PURE__ */ jsx_runtime.jsx("input", {
id: "netxops-alarm-im",
type: "checkbox",
checked: state.alarmDeliverIm.text === "true",
disabled,
onChange: (event) => {
props.edit("alarmDeliverIm", event.target.checked ? "true" : "false");
}
}),
/* @__PURE__ */ jsx_runtime.jsx("span", {
children: t("alarmDeliverImHint")
})
]
})
]
}),
/* @__PURE__ */ jsx_runtime.jsx(ValueField, {
id: "netxops-im-bot-id",
label: t("imBotId"),
hint: t("imBotIdHint"),
field: state.imBotId,
overriddenLabel: t("overridden"),
resetLabel: t("reset"),
invalidLabel: t("invalid"),
disabled,
onEdit: (text) => {
props.edit("imBotId", text);
},
onReset: () => {
props.resetField("imBotId");
}
}),
/* @__PURE__ */ jsx_runtime.jsx(ValueField, {
id: "netxops-im-target-id",
label: t("imTargetId"),
hint: t("imTargetIdHint"),
field: state.imTargetId,
overriddenLabel: t("overridden"),
resetLabel: t("reset"),
invalidLabel: t("invalid"),
disabled,
onEdit: (text) => {
props.edit("imTargetId", text);
},
onReset: () => {
props.resetField("imTargetId");
}
}),
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-footer",
children: [
@ -533,6 +667,22 @@ function booleanField(field) {
}
};
}
function booleanFieldPersistFalse(field) {
return {
field,
format: (value) => value === true ? "true" : "false",
parse: (text) => {
const normalized = text.trim().toLowerCase();
if (normalized === "true" || normalized === "1" || normalized === "yes") {
return { kind: "set", value: true };
}
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "") {
return { kind: "set", value: false };
}
return;
}
};
}
class CardForm {
scope;
@ -716,7 +866,15 @@ class NetxopsCardController {
constructor(scope, ctx) {
this.scope = scope;
this.ctx = ctx;
this.form = new CardForm(scope, [textField("apiUrl"), textField("lang"), booleanField("alarmPushEnabled")], [{ field: API_TOKEN_FIELD, write: (text) => this.writeToken(text) }]);
this.form = new CardForm(scope, [
textField("apiUrl"),
textField("lang"),
booleanField("alarmPushEnabled"),
booleanFieldPersistFalse("alarmDeliverDsh"),
booleanField("alarmDeliverIm"),
textField("imBotId"),
textField("imTargetId")
], [{ field: API_TOKEN_FIELD, write: (text) => this.writeToken(text) }]);
this.store = this.form.bind(() => this.projection());
scope.subscribe(() => {
this.readCredential();
@ -783,6 +941,10 @@ class NetxopsCardController {
apiUrl: this.form.field("apiUrl"),
lang: this.form.field("lang"),
alarmPushEnabled: this.form.field("alarmPushEnabled"),
alarmDeliverDsh: this.form.field("alarmDeliverDsh"),
alarmDeliverIm: this.form.field("alarmDeliverIm"),
imBotId: this.form.field("imBotId"),
imTargetId: this.form.field("imTargetId"),
apiToken: this.form.field(API_TOKEN_FIELD),
apiTokenConfigured: this.credential.configured,
apiTokenWritable: this.credential.remoteReady && this.credential.writable,
@ -858,7 +1020,7 @@ var en = {
lang: "Language",
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.",
alarmPushEnabledHint: "Dial out to netx when a matched key alarm arrives. Choose DSH and/or IM sinks below.",
alarmPushStatus: "Push link",
alarmPushPhaseDisabled: "Off",
alarmPushPhaseIdle: "Idle",
@ -868,6 +1030,14 @@ var en = {
alarmPushPhaseReconnecting: "Reconnecting…",
alarmPushPhaseAuthFailed: "Auth failed",
alarmPushPhaseError: "Error",
alarmDeliverDsh: "Deliver to DSH session",
alarmDeliverDshHint: "Open/follow the sticky「Netx 关键告警」session on this Host.",
alarmDeliverIm: "Deliver to WhatsApp / IM",
alarmDeliverImHint: "Requires dsh-im-ops. Paste botId + targetId from IM → 投递设置 → 复制调用参数.",
imBotId: "IM Bot ID",
imBotIdHint: "Opaque botId from IM delivery settings (not a phone number).",
imTargetId: "IM Target ID",
imTargetIdHint: "Opaque targetId alias (e.g. release-alerts). Create the target in IM first.",
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.",
@ -893,7 +1063,7 @@ var zh = {
lang: "语言",
langHint: "响应语言提示(zh / en)。",
alarmPushEnabled: "关键告警推送",
alarmPushEnabledHint: "主动连接 netx;匹配到关键告警后在本机打开/续写一条 DSH 会话。不必安装 WhatsApp / im。",
alarmPushEnabledHint: "主动连接 netx;匹配到关键告警后按下述开关投递。可只开 DSH、只开 IM,或两者都开。",
alarmPushStatus: "推送链路",
alarmPushPhaseDisabled: "未开启",
alarmPushPhaseIdle: "空闲",
@ -903,6 +1073,14 @@ var zh = {
alarmPushPhaseReconnecting: "重连中…",
alarmPushPhaseAuthFailed: "鉴权失败",
alarmPushPhaseError: "异常",
alarmDeliverDsh: "投递到 DSH 会话",
alarmDeliverDshHint: "在本机打开/续写 sticky「Netx 关键告警」会话。",
alarmDeliverIm: "投递到 WhatsApp / IM",
alarmDeliverImHint: "需要已安装 dsh-im-ops。请先在 IM「投递设置」新建目标,再粘贴「复制调用参数」里的 botId / targetId。",
imBotId: "IM Bot ID",
imBotIdHint: "来自 IM 投递设置的不透明 botId(不是手机号)。",
imTargetId: "IM Target ID",
imTargetIdHint: "投递目标别名(如 release-alerts)。请先在 IM 侧创建目标。",
apiToken: "API Token",
apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。",
apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。",