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

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