mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 10:50:46 +08:00
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:
parent
e8b7d7331f
commit
83c3796fbb
12 changed files with 482 additions and 23 deletions
|
|
@ -16,7 +16,7 @@ dsh web
|
|||
Then:
|
||||
|
||||
1. Settings → **Plugins** → **Netx Ops** → API URL / token
|
||||
2. Optional: enable **关键告警推送** — DSH dials out to netx and opens a sticky session on matched key alarms (WhatsApp / im not required). The card shows live WSS status (Connected / Reconnecting / Auth failed).
|
||||
2. Optional: enable **关键告警推送** — DSH dials out to netx; choose **Deliver to DSH session** and/or **Deliver to WhatsApp / IM** (paste `botId`+`targetId` from IM 投递设置). The card shows live WSS status.
|
||||
3. Settings → **Agent presets** → Custom → **Netx Ops** (installed automatically on first host activate)
|
||||
4. New session → choose **Netx Ops**
|
||||
|
||||
|
|
|
|||
|
|
@ -14,3 +14,7 @@
|
|||
toolCallTimeoutMs: 120000
|
||||
installAgentPreset: true
|
||||
alarmPushEnabled: false
|
||||
alarmDeliverDsh: true
|
||||
alarmDeliverIm: false
|
||||
imBotId: ''
|
||||
imTargetId: ''
|
||||
|
|
|
|||
|
|
@ -30,16 +30,16 @@ dsh web # or: pnpm dsh web
|
|||
|
||||
1. **Settings → Plugins → Netx Ops** → API URL (+ token if the field is enabled).
|
||||
Token fallback: `scripts/set-netx-token.ps1` / `.sh`.
|
||||
Optional: enable **关键告警推送** so this DSH dials `ws(s)://<apiUrl>/v1/integrations/dsh-alarm/ws` and follows a sticky session when netx matches a key alert. The card header shows live WSS status (`Connected` / `Reconnecting` / `Auth failed`).
|
||||
Optional: enable **关键告警推送** so this DSH dials `ws(s)://<apiUrl>/v1/integrations/dsh-alarm/ws`. Choose **投递到 DSH 会话** and/or **投递到 WhatsApp / IM**. For IM, install `dsh-im-ops`, create a delivery target, then paste `imBotId` / `imTargetId`. The card header shows live WSS status.
|
||||
2. Restart or open Settings → **Agent presets** → Custom → **Netx Ops** should appear after the host plugin has activated once.
|
||||
3. **New session → preset Netx Ops** → ask e.g. Critical Top / single-host alarms.
|
||||
|
||||
## Key-alarm push
|
||||
|
||||
- **netx** (fixed IP) hosts the subscribe hub and fans out matched key alerts.
|
||||
- **netxops** (each DSH) reuses the same URL/token, connects outbound, and delivers into a DSH session.
|
||||
- **netxops** (each DSH) reuses the same URL/token, connects outbound, and delivers into a DSH session and/or IM via `ctx.dshIm.send`.
|
||||
- Live status is exposed on the Plugins card via Connection RPC (`/netxops` → `alarm-push.status`); no curl required for day-to-day checks.
|
||||
- WhatsApp / `dsh-im` is **not** required for this path.
|
||||
- WhatsApp / `dsh-im` is **optional** for the DSH session path; required only when IM delivery is checked.
|
||||
|
||||
## Verify
|
||||
|
||||
|
|
|
|||
184
lib/client.js
184
lib/client.js
|
|
@ -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 在监视凭据文件则无需重启。",
|
||||
|
|
|
|||
55
lib/index.js
55
lib/index.js
|
|
@ -250,6 +250,30 @@ function startAlarmPushClient(options) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/alarm-im.ts
|
||||
async function deliverAlarmToIm(ctx, payload, options) {
|
||||
if (!options.enabled)
|
||||
return;
|
||||
const botId = options.botId.trim();
|
||||
const targetId = options.targetId.trim();
|
||||
if (!botId || !targetId) {
|
||||
ctx.logger.warn("netxops alarm-im: enabled but imBotId/imTargetId empty — skip IM delivery (create a target in IM 投递设置 and paste botId+targetId)");
|
||||
return;
|
||||
}
|
||||
const im = (typeof ctx.get === "function" ? ctx.get("dshIm") : undefined) ?? ctx.dshIm;
|
||||
if (!im || typeof im.send !== "function") {
|
||||
ctx.logger.warn("netxops alarm-im: ctx.dshIm unavailable — install/enable dsh-im-ops to deliver alarms to WhatsApp/IM");
|
||||
return;
|
||||
}
|
||||
const text = formatAlarmPrompt(payload, options.lang);
|
||||
try {
|
||||
await im.send(botId, targetId, text);
|
||||
ctx.logger.info("netxops alarm-im: sent to botId=%s targetId=%s", botId, targetId);
|
||||
} catch (error) {
|
||||
ctx.logger.warn("netxops alarm-im: send failed: %s", error);
|
||||
}
|
||||
}
|
||||
|
||||
// src/netx/alarm-session.ts
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
|
@ -400,7 +424,11 @@ var Config = z.object({
|
|||
tokenCredentialRef: z.string().role("credential-ref").default(DEFAULT_TOKEN_REF),
|
||||
toolCallTimeoutMs: z.number().step(1).min(1000).default(120000),
|
||||
installAgentPreset: z.boolean().default(true),
|
||||
alarmPushEnabled: z.boolean().default(false)
|
||||
alarmPushEnabled: z.boolean().default(false),
|
||||
alarmDeliverDsh: z.boolean().default(true),
|
||||
alarmDeliverIm: z.boolean().default(false),
|
||||
imBotId: z.string().default(""),
|
||||
imTargetId: z.string().default("")
|
||||
});
|
||||
function packageRoot() {
|
||||
return join2(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
|
@ -455,11 +483,11 @@ function apply(ctx, config = Config({})) {
|
|||
if (config.installAgentPreset) {
|
||||
ensureAgentPresetInstalled(ctx.logger);
|
||||
}
|
||||
const restartAlarmPush = (apiUrl, token, enabled, lang) => {
|
||||
const restartAlarmPush = (current, apiUrl, token) => {
|
||||
stopAlarmPush?.();
|
||||
stopAlarmPush = undefined;
|
||||
resetAlarmSession();
|
||||
if (!enabled) {
|
||||
if (!current.alarmPushEnabled) {
|
||||
resetAlarmPushStatus();
|
||||
return;
|
||||
}
|
||||
|
|
@ -474,11 +502,26 @@ function apply(ctx, config = Config({})) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
const lang = current.lang;
|
||||
const deliverDsh = current.alarmDeliverDsh !== false;
|
||||
const deliverIm = current.alarmDeliverIm === true;
|
||||
const imBotId = current.imBotId ?? "";
|
||||
const imTargetId = current.imTargetId ?? "";
|
||||
stopAlarmPush = startAlarmPushClient({
|
||||
apiUrl,
|
||||
token,
|
||||
logger: ctx.logger,
|
||||
onAlarm: (payload) => deliverAlarmToSession(ctx, payload, lang)
|
||||
onAlarm: async (payload) => {
|
||||
if (deliverDsh) {
|
||||
await deliverAlarmToSession(ctx, payload, lang);
|
||||
}
|
||||
await deliverAlarmToIm(ctx, payload, {
|
||||
enabled: deliverIm,
|
||||
botId: imBotId,
|
||||
targetId: imTargetId,
|
||||
lang
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const publish = () => {
|
||||
|
|
@ -496,11 +539,11 @@ function apply(ctx, config = Config({})) {
|
|||
lang: current.lang,
|
||||
toolCallTimeoutMs: current.toolCallTimeoutMs
|
||||
});
|
||||
restartAlarmPush(apiUrl, token, current.alarmPushEnabled === true, current.lang);
|
||||
restartAlarmPush(current, apiUrl, token);
|
||||
if (!tokenConfigured) {
|
||||
ctx.logger.warn("netxops: published connection → %s tokenConfigured=false (set credential %s)", apiUrl, current.tokenCredentialRef);
|
||||
} else {
|
||||
ctx.logger.info("netxops: published connection → %s tokenConfigured=true alarmPush=%s", apiUrl, current.alarmPushEnabled === true);
|
||||
ctx.logger.info("netxops: published connection → %s tokenConfigured=true alarmPush=%s dsh=%s im=%s", apiUrl, current.alarmPushEnabled === true, current.alarmDeliverDsh !== false, current.alarmDeliverIm === true);
|
||||
}
|
||||
}).catch((error) => {
|
||||
ctx.logger.error("netxops: connection publish error: %s", error);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "dsh-netxops",
|
||||
"version": "0.1.16",
|
||||
"version": "0.1.17",
|
||||
"description": "DeepSeek Harness Netx Ops: Ops-scoped netx__* REST tools + key-alarm push to DSH sessions + Plugins settings card + agent preset",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -236,6 +236,84 @@ export function NetxopsCard(props: NetxopsCardProps) {
|
|||
? <p className="dsh-nx-invalid" role="status">{pushStatus.lastError}</p>
|
||||
: null}
|
||||
</div>
|
||||
<div className="dsh-nx-field">
|
||||
<div className="dsh-nx-fieldHead">
|
||||
<label className="dsh-nx-label" htmlFor="netxops-alarm-dsh">{t('alarmDeliverDsh')}</label>
|
||||
{state.alarmDeliverDsh.overridden
|
||||
? (
|
||||
<span className="dsh-nx-badges">
|
||||
<span className="dsh-nx-badge">{t('overridden')}</span>
|
||||
<button type="button" className="dsh-nx-reset" disabled={disabled} onClick={() => { props.resetField('alarmDeliverDsh') }}>
|
||||
{t('reset')}
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<label className="dsh-nx-checkRow" htmlFor="netxops-alarm-dsh">
|
||||
<input
|
||||
id="netxops-alarm-dsh"
|
||||
type="checkbox"
|
||||
checked={state.alarmDeliverDsh.text === 'true'}
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
props.edit('alarmDeliverDsh', event.target.checked ? 'true' : 'false')
|
||||
}}
|
||||
/>
|
||||
<span>{t('alarmDeliverDshHint')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="dsh-nx-field">
|
||||
<div className="dsh-nx-fieldHead">
|
||||
<label className="dsh-nx-label" htmlFor="netxops-alarm-im">{t('alarmDeliverIm')}</label>
|
||||
{state.alarmDeliverIm.overridden
|
||||
? (
|
||||
<span className="dsh-nx-badges">
|
||||
<span className="dsh-nx-badge">{t('overridden')}</span>
|
||||
<button type="button" className="dsh-nx-reset" disabled={disabled} onClick={() => { props.resetField('alarmDeliverIm') }}>
|
||||
{t('reset')}
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<label className="dsh-nx-checkRow" htmlFor="netxops-alarm-im">
|
||||
<input
|
||||
id="netxops-alarm-im"
|
||||
type="checkbox"
|
||||
checked={state.alarmDeliverIm.text === 'true'}
|
||||
disabled={disabled}
|
||||
onChange={(event) => {
|
||||
props.edit('alarmDeliverIm', event.target.checked ? 'true' : 'false')
|
||||
}}
|
||||
/>
|
||||
<span>{t('alarmDeliverImHint')}</span>
|
||||
</label>
|
||||
</div>
|
||||
<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={disabled}
|
||||
onEdit={(text) => { props.edit('imBotId', text) }}
|
||||
onReset={() => { props.resetField('imBotId') }}
|
||||
/>
|
||||
<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={disabled}
|
||||
onEdit={(text) => { props.edit('imTargetId', text) }}
|
||||
onReset={() => { props.resetField('imTargetId') }}
|
||||
/>
|
||||
<div className="dsh-nx-footer">
|
||||
{state.failed ? <p className="dsh-nx-failed" role="status">{t('saveFailed')}</p> : null}
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -80,6 +80,27 @@ export function booleanField(field: string): CardFieldSpec {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean that persists `false` explicitly (use when schema default is `true`).
|
||||
* Clearing would restore the default and make "off" impossible.
|
||||
*/
|
||||
export function booleanFieldPersistFalse(field: string): CardFieldSpec {
|
||||
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 undefined
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export class CardForm<T> {
|
||||
private readonly specs: Map<string, CardFieldSpec>
|
||||
private readonly secretSpecs: Map<string, CardSecretSpec>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|||
import type { SettingsScope, SettingsScopeSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client'
|
||||
import type { SnapshotStore } from './snapshot-store.ts'
|
||||
import {
|
||||
CardForm, textField, booleanField,
|
||||
CardForm, textField, booleanField, booleanFieldPersistFalse,
|
||||
type CardActions, type CardFieldState, type CardShell,
|
||||
} from './card-form.ts'
|
||||
import {
|
||||
|
|
@ -26,6 +26,10 @@ export interface NetxopsSettings {
|
|||
lang?: string
|
||||
tokenCredentialRef?: string
|
||||
alarmPushEnabled?: boolean
|
||||
alarmDeliverDsh?: boolean
|
||||
alarmDeliverIm?: boolean
|
||||
imBotId?: string
|
||||
imTargetId?: string
|
||||
}
|
||||
|
||||
interface CredentialState {
|
||||
|
|
@ -39,6 +43,10 @@ export interface NetxopsCardState extends CardShell {
|
|||
apiUrl: CardFieldState
|
||||
lang: CardFieldState
|
||||
alarmPushEnabled: CardFieldState
|
||||
alarmDeliverDsh: CardFieldState
|
||||
alarmDeliverIm: CardFieldState
|
||||
imBotId: CardFieldState
|
||||
imTargetId: CardFieldState
|
||||
apiToken: CardFieldState
|
||||
apiTokenConfigured: boolean
|
||||
apiTokenWritable: boolean
|
||||
|
|
@ -78,7 +86,15 @@ export class NetxopsCardController {
|
|||
) {
|
||||
this.form = new CardForm(
|
||||
scope,
|
||||
[textField('apiUrl'), textField('lang'), booleanField('alarmPushEnabled')],
|
||||
[
|
||||
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())
|
||||
|
|
@ -158,6 +174,10 @@ export 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,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,14 @@ export type NetxopsLocaleKey =
|
|||
| 'alarmPushPhaseReconnecting'
|
||||
| 'alarmPushPhaseAuthFailed'
|
||||
| 'alarmPushPhaseError'
|
||||
| 'alarmDeliverDsh'
|
||||
| 'alarmDeliverDshHint'
|
||||
| 'alarmDeliverIm'
|
||||
| 'alarmDeliverImHint'
|
||||
| 'imBotId'
|
||||
| 'imBotIdHint'
|
||||
| 'imTargetId'
|
||||
| 'imTargetIdHint'
|
||||
| 'apiToken'
|
||||
| 'apiTokenHint'
|
||||
| 'apiTokenUnavailable'
|
||||
|
|
@ -43,7 +51,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
|||
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',
|
||||
|
|
@ -53,6 +61,14 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
|||
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.',
|
||||
|
|
@ -79,7 +95,7 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
|||
lang: '语言',
|
||||
langHint: '响应语言提示(zh / en)。',
|
||||
alarmPushEnabled: '关键告警推送',
|
||||
alarmPushEnabledHint: '主动连接 netx;匹配到关键告警后在本机打开/续写一条 DSH 会话。不必安装 WhatsApp / im。',
|
||||
alarmPushEnabledHint: '主动连接 netx;匹配到关键告警后按下述开关投递。可只开 DSH、只开 IM,或两者都开。',
|
||||
alarmPushStatus: '推送链路',
|
||||
alarmPushPhaseDisabled: '未开启',
|
||||
alarmPushPhaseIdle: '空闲',
|
||||
|
|
@ -89,6 +105,14 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
|||
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 在监视凭据文件则无需重启。',
|
||||
|
|
|
|||
43
src/index.ts
43
src/index.ts
|
|
@ -25,6 +25,7 @@ import {
|
|||
publishAlarmPushStatus,
|
||||
resetAlarmPushStatus,
|
||||
} from './netx/alarm-push-status.ts'
|
||||
import { deliverAlarmToIm } from './netx/alarm-im.ts'
|
||||
import { deliverAlarmToSession, resetAlarmSession } from './netx/alarm-session.ts'
|
||||
import { publishNetxConnection } from './netx/runtime.ts'
|
||||
|
||||
|
|
@ -66,6 +67,17 @@ export interface Config {
|
|||
* alarms into a sticky DSH session.
|
||||
*/
|
||||
alarmPushEnabled: boolean
|
||||
/** When push is on, also followup the sticky Netx Ops DSH session (default). */
|
||||
alarmDeliverDsh: boolean
|
||||
/**
|
||||
* When push is on, also send via `ctx.dshIm.send(imBotId, imTargetId, …)`.
|
||||
* Requires dsh-im-ops and a saved delivery target.
|
||||
*/
|
||||
alarmDeliverIm: boolean
|
||||
/** Opaque bot id from IM「投递设置」→ 复制调用参数. */
|
||||
imBotId: string
|
||||
/** Opaque target id from the same copy payload. */
|
||||
imTargetId: string
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
|
|
@ -75,6 +87,10 @@ export const Config: z<Config> = z.object({
|
|||
toolCallTimeoutMs: z.number().step(1).min(1000).default(120_000),
|
||||
installAgentPreset: z.boolean().default(true),
|
||||
alarmPushEnabled: z.boolean().default(false),
|
||||
alarmDeliverDsh: z.boolean().default(true),
|
||||
alarmDeliverIm: z.boolean().default(false),
|
||||
imBotId: z.string().default(''),
|
||||
imTargetId: z.string().default(''),
|
||||
})
|
||||
|
||||
/** Package root (parent of `lib/` or `src/` depending on launch). */
|
||||
|
|
@ -171,11 +187,11 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
|||
ensureAgentPresetInstalled(ctx.logger)
|
||||
}
|
||||
|
||||
const restartAlarmPush = (apiUrl: string, token: string, enabled: boolean, lang: string): void => {
|
||||
const restartAlarmPush = (current: Config, apiUrl: string, token: string): void => {
|
||||
stopAlarmPush?.()
|
||||
stopAlarmPush = undefined
|
||||
resetAlarmSession()
|
||||
if (!enabled) {
|
||||
if (!current.alarmPushEnabled) {
|
||||
resetAlarmPushStatus()
|
||||
return
|
||||
}
|
||||
|
|
@ -190,11 +206,26 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
|||
})
|
||||
return
|
||||
}
|
||||
const lang = current.lang
|
||||
const deliverDsh = current.alarmDeliverDsh !== false
|
||||
const deliverIm = current.alarmDeliverIm === true
|
||||
const imBotId = current.imBotId ?? ''
|
||||
const imTargetId = current.imTargetId ?? ''
|
||||
stopAlarmPush = startAlarmPushClient({
|
||||
apiUrl,
|
||||
token,
|
||||
logger: ctx.logger,
|
||||
onAlarm: (payload) => deliverAlarmToSession(ctx, payload, lang),
|
||||
onAlarm: async (payload) => {
|
||||
if (deliverDsh) {
|
||||
await deliverAlarmToSession(ctx, payload, lang)
|
||||
}
|
||||
await deliverAlarmToIm(ctx, payload, {
|
||||
enabled: deliverIm,
|
||||
botId: imBotId,
|
||||
targetId: imTargetId,
|
||||
lang,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +244,7 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
|||
lang: current.lang,
|
||||
toolCallTimeoutMs: current.toolCallTimeoutMs,
|
||||
})
|
||||
restartAlarmPush(apiUrl, token, current.alarmPushEnabled === true, current.lang)
|
||||
restartAlarmPush(current, apiUrl, token)
|
||||
if (!tokenConfigured) {
|
||||
ctx.logger.warn(
|
||||
'netxops: published connection → %s tokenConfigured=false (set credential %s)',
|
||||
|
|
@ -222,9 +253,11 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
|||
)
|
||||
} else {
|
||||
ctx.logger.info(
|
||||
'netxops: published connection → %s tokenConfigured=true alarmPush=%s',
|
||||
'netxops: published connection → %s tokenConfigured=true alarmPush=%s dsh=%s im=%s',
|
||||
apiUrl,
|
||||
current.alarmPushEnabled === true,
|
||||
current.alarmDeliverDsh !== false,
|
||||
current.alarmDeliverIm === true,
|
||||
)
|
||||
}
|
||||
}).catch((error) => {
|
||||
|
|
|
|||
58
src/netx/alarm-im.ts
Normal file
58
src/netx/alarm-im.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* Optional IM sink for key alarms (soft-depends on dsh-im `ctx.dshIm`).
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { formatAlarmPrompt, type KeyAlarmPayload } from './alarm-push.ts'
|
||||
|
||||
export interface AlarmImDeliveryOptions {
|
||||
enabled: boolean
|
||||
botId: string
|
||||
targetId: string
|
||||
lang: string
|
||||
}
|
||||
|
||||
type DshIm = {
|
||||
send: (botId: string, targetId: string, text: string, opts?: { signal?: AbortSignal }) => Promise<unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a formatted key-alarm text through proactive IM delivery when configured.
|
||||
* @param ctx - host cordis context (may lack `dshIm`).
|
||||
* @param payload - matched alarm.
|
||||
* @param options - settings snapshot for IM sink.
|
||||
*/
|
||||
export async function deliverAlarmToIm(
|
||||
ctx: Context,
|
||||
payload: KeyAlarmPayload,
|
||||
options: AlarmImDeliveryOptions,
|
||||
): Promise<void> {
|
||||
if (!options.enabled) return
|
||||
const botId = options.botId.trim()
|
||||
const targetId = options.targetId.trim()
|
||||
if (!botId || !targetId) {
|
||||
ctx.logger.warn(
|
||||
'netxops alarm-im: enabled but imBotId/imTargetId empty — skip IM delivery (create a target in IM 投递设置 and paste botId+targetId)',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Soft inject: im plugin may be absent on this profile.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const im = (typeof (ctx as any).get === 'function' ? (ctx as any).get('dshIm') : undefined) as DshIm | undefined
|
||||
?? ((ctx as { dshIm?: DshIm }).dshIm)
|
||||
if (!im || typeof im.send !== 'function') {
|
||||
ctx.logger.warn(
|
||||
'netxops alarm-im: ctx.dshIm unavailable — install/enable dsh-im-ops to deliver alarms to WhatsApp/IM',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const text = formatAlarmPrompt(payload, options.lang)
|
||||
try {
|
||||
await im.send(botId, targetId, text)
|
||||
ctx.logger.info('netxops alarm-im: sent to botId=%s targetId=%s', botId, targetId)
|
||||
} catch (error) {
|
||||
ctx.logger.warn('netxops alarm-im: send failed: %s', error)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue