From 12f7018103e95727b590309632905075f94f139e Mon Sep 17 00:00:00 2001 From: oliver Date: Sat, 5 Sep 2026 20:50:51 +0800 Subject: [PATCH] Add key-alarm push: reuse URL/token, dial netx hub, sticky DSH session (0.1.15). Plugins card gains an alarm-push toggle; when enabled, netxops subscribes outbound and followups a Netx Ops session without requiring im/WhatsApp. Co-authored-by: Cursor --- README.md | 6 +- cordis.patch.yml | 1 + docs/INSTALL.md | 9 +- lib/client.js | 80 ++++++++- lib/index.js | 334 +++++++++++++++++++++++++++++++++++-- package.json | 4 +- scripts/build-host.mjs | 9 + src/client/NetxopsCard.tsx | 32 ++++ src/client/card-form.ts | 18 ++ src/client/controller.ts | 7 +- src/client/locales.ts | 10 +- src/client/styles.ts | 2 + src/index.ts | 49 +++++- src/netx/alarm-push.ts | 220 ++++++++++++++++++++++++ src/netx/alarm-session.ts | 180 ++++++++++++++++++++ test/alarm-push.test.mjs | 36 ++++ 16 files changed, 966 insertions(+), 31 deletions(-) create mode 100644 src/netx/alarm-push.ts create mode 100644 src/netx/alarm-session.ts create mode 100644 test/alarm-push.test.mjs diff --git a/README.md b/README.md index 79eadbe..d886189 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ dsh web Then: 1. Settings → **Plugins** → **Netx Ops** → API URL / token -2. Settings → **Agent presets** → Custom → **Netx Ops** (installed automatically on first host activate) -3. New session → choose **Netx Ops** +2. Optional: enable **关键告警推送** — DSH dials out to netx and opens a sticky session on matched key alarms (WhatsApp / im not required) +3. Settings → **Agent presets** → Custom → **Netx Ops** (installed automatically on first host activate) +4. New session → choose **Netx Ops** Also need: a reachable **netx API** (no local `pip install netx_mcp`). Details: [docs/INSTALL.md](docs/INSTALL.md). @@ -26,6 +27,7 @@ Also need: a reachable **netx API** (no local `pip install netx_mcp`). Details: | In the plugin | Outside (data / runtime) | |---------------|---------------------------| | `netx__*` REST tools (Ops preset only) + settings + credentials | netx HTTP API (URL + token) | +| Optional key-alarm push (WSS client → sticky DSH session) | netx `/v1/integrations/dsh-alarm/ws` hub | | Persona + UME / managed-NE skills | | | Agent preset auto-install to `~/.dsh/.agent-presets` | | diff --git a/cordis.patch.yml b/cordis.patch.yml index cbf0a23..79a92d9 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -13,3 +13,4 @@ tokenCredentialRef: NETX_API_TOKEN toolCallTimeoutMs: 120000 installAgentPreset: true + alarmPushEnabled: false diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 482e153..7f77588 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -29,10 +29,17 @@ 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`. + Token fallback: `scripts/set-netx-token.ps1` / `.sh`. + Optional: enable **关键告警推送** so this DSH dials `ws(s):///v1/integrations/dsh-alarm/ws` and follows a sticky session when netx matches a key alert. 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. +- WhatsApp / `dsh-im` is **not** required for this path. + ## Verify 1. Plugins card **Netx Ops** visible. diff --git a/lib/client.js b/lib/client.js index fafbce1..408cef3 100644 --- a/lib/client.js +++ b/lib/client.js @@ -82,6 +82,8 @@ var CSS = ` .dsh-nx-input:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)} .dsh-nx-inputInvalid{border-color:var(--dsw-alias-label-error)} .dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)} +.dsh-nx-checkRow{display:flex;align-items:flex-start;gap:10px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer} +.dsh-nx-checkRow input{margin-top:2px;flex:none} .dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)} .dsh-nx-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 0 4px;border-top:.5px solid var(--dsw-alias-border-l2)} .dsh-nx-failed{flex:1;min-width:0;margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)} @@ -287,6 +289,57 @@ function NetxopsCard(props) { props.resetField("lang"); } }), + /* @__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-push", + children: t("alarmPushEnabled") + }), + state.alarmPushEnabled.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("alarmPushEnabled"); + }, + children: t("reset") + }) + ] + }) : null + ] + }), + /* @__PURE__ */ jsx_runtime.jsxs("label", { + className: "dsh-nx-checkRow", + htmlFor: "netxops-alarm-push", + children: [ + /* @__PURE__ */ jsx_runtime.jsx("input", { + id: "netxops-alarm-push", + type: "checkbox", + checked: state.alarmPushEnabled.text === "true", + disabled, + onChange: (event) => { + props.edit("alarmPushEnabled", event.target.checked ? "true" : "false"); + } + }), + /* @__PURE__ */ jsx_runtime.jsx("span", { + children: t("alarmPushEnabledHint") + }) + ] + }) + ] + }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-footer", children: [ @@ -348,6 +401,22 @@ function textField(field) { } }; } +function booleanField(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: "clear" }; + } + return; + } + }; +} class CardForm { scope; @@ -526,7 +595,7 @@ class NetxopsCardController { constructor(scope, ctx) { this.scope = scope; this.ctx = ctx; - this.form = new CardForm(scope, [textField("apiUrl"), textField("lang")], [{ field: API_TOKEN_FIELD, write: (text) => this.writeToken(text) }]); + this.form = new CardForm(scope, [textField("apiUrl"), textField("lang"), booleanField("alarmPushEnabled")], [{ field: API_TOKEN_FIELD, write: (text) => this.writeToken(text) }]); this.store = this.form.bind(() => this.projection()); scope.subscribe(() => { this.readCredential(); @@ -550,6 +619,7 @@ class NetxopsCardController { ...this.form.shell(), apiUrl: this.form.field("apiUrl"), lang: this.form.field("lang"), + alarmPushEnabled: this.form.field("alarmPushEnabled"), apiToken: this.form.field(API_TOKEN_FIELD), apiTokenConfigured: this.credential.configured, apiTokenWritable: this.credential.remoteReady && this.credential.writable, @@ -620,9 +690,11 @@ var en = { title: "Netx Ops", description: "UME API endpoint and bearer token for netx__* tools.", apiUrl: "API URL", - apiUrlHint: "netx REST root, e.g. http://127.0.0.1:8890", + apiUrlHint: "netx REST root, e.g. http://127.0.0.1:8890 (also used for alarm subscribe)", 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.", 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.", @@ -644,9 +716,11 @@ var zh = { title: "Netx Ops", description: "UME API 地址与 Bearer Token,供 netx__* 工具使用。", apiUrl: "API 地址", - apiUrlHint: "netx REST 根地址,例如 http://127.0.0.1:8890", + apiUrlHint: "netx REST 根地址,例如 http://127.0.0.1:8890(告警订阅复用同一地址)", lang: "语言", langHint: "响应语言提示(zh / en)。", + alarmPushEnabled: "关键告警推送", + alarmPushEnabledHint: "主动连接 netx;匹配到关键告警后在本机打开/续写一条 DSH 会话。不必安装 WhatsApp / im。", apiToken: "API Token", apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。", apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。", diff --git a/lib/index.js b/lib/index.js index 5fe777a..4dad28e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,12 +1,301 @@ +import { createRequire } from "node:module"; +var __require = /* @__PURE__ */ createRequire(import.meta.url); + // src/index.ts import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join } from "node:path"; +import { homedir as homedir2 } from "node:os"; +import { dirname, join as join2 } from "node:path"; import { fileURLToPath } from "node:url"; 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.ts +function alarmSubscribeUrl(apiUrl) { + const trimmed = apiUrl.trim().replace(/\/$/, ""); + if (!trimmed) + return ""; + let url; + try { + url = new URL(trimmed); + } catch { + return ""; + } + url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; + url.pathname = "/v1/integrations/dsh-alarm/ws"; + url.search = ""; + url.hash = ""; + return url.toString(); +} +function formatAlarmPrompt(payload, lang = "zh") { + const action = String(payload.action ?? "").trim().toLowerCase(); + const actionZh = { + inserted: "告警产生", + updated: "告警更新", + deleted: "告警清除" + }; + const actionEn = { + inserted: "Alarm Raised", + updated: "Alarm Updated", + deleted: "Alarm Cleared" + }; + const ne = payload.ne && typeof payload.ne === "object" ? payload.ne : {}; + const host = String(ne.host_name ?? "").trim(); + const ip = String(ne.ip_address ?? "").trim(); + const neName = String(ne.ne_name ?? ne.user_label ?? "").trim(); + let device = host || neName || String(payload.ne_id ?? "").trim() || "-"; + if (ip) + device = device === "-" ? ip : `${device} (${ip})`; + const label = String(payload.rule_label ?? payload.native_probable_cause ?? "关键告警").trim(); + if (lang.startsWith("en")) { + return [ + `[UME ${actionEn[action] ?? (action || "Alarm")}] ${label}`, + `Device: ${device}`, + `Object: ${String(payload.object_name ?? "-").trim()}`, + `Severity: ${String(payload.perceived_severity ?? "-").trim()}`, + `Cause: ${String(payload.native_probable_cause ?? "-").trim()}`, + `Time: ${String(payload.time_created ?? "-").trim()}`, + `notificationId: ${String(payload.notification_id ?? "-").trim()}`, + `alarm_key: ${String(payload.alarm_key ?? "-").trim()}`, + "", + "Please analyze this key alarm and suggest next ops steps." + ].join(` +`); + } + return [ + `[UME ${actionZh[action] ?? (action || "告警")}] ${label}`, + `设备: ${device}`, + `对象: ${String(payload.object_name ?? "-").trim()}`, + `级别: ${String(payload.perceived_severity ?? "-").trim()}`, + `原因: ${String(payload.native_probable_cause ?? "-").trim()}`, + `时间: ${String(payload.time_created ?? "-").trim()}`, + `notificationId: ${String(payload.notification_id ?? "-").trim()}`, + `alarm_key: ${String(payload.alarm_key ?? "-").trim()}`, + "", + "请分析这条关键告警并给出下一步运维建议。" + ].join(` +`); +} +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"); + return () => {}; + } + const WS = options.WebSocketImpl ?? globalThis.WebSocket; + if (typeof WS !== "function") { + log.error?.("netxops alarm-push: WebSocket is unavailable in this runtime"); + return () => {}; + } + let closed = false; + let socket = null; + let reconnectTimer; + let attempt = 0; + const baseDelay = Math.max(500, options.reconnectMs ?? 2000); + const clearReconnect = () => { + if (reconnectTimer !== undefined) { + clearTimeout(reconnectTimer); + reconnectTimer = undefined; + } + }; + const scheduleReconnect = () => { + if (closed) + return; + clearReconnect(); + const delay = Math.min(60000, baseDelay * 2 ** Math.min(attempt, 5)); + attempt += 1; + reconnectTimer = setTimeout(() => { + connect(); + }, delay); + }; + const connect = () => { + if (closed) + return; + clearReconnect(); + try { + socket = new WS(wsUrl); + } catch (error) { + log.warn?.("netxops alarm-push: connect failed:", error); + scheduleReconnect(); + return; + } + socket.addEventListener("open", () => { + attempt = 0; + socket?.send(JSON.stringify({ type: "auth", token })); + }); + socket.addEventListener("message", (event) => { + let msg; + try { + msg = JSON.parse(String(event.data)); + } catch { + return; + } + const type = String(msg.type ?? "").toLowerCase(); + if (type === "auth-ok") { + log.info?.("netxops alarm-push: subscribed to %s", wsUrl); + return; + } + if (type === "auth-fail") { + log.error?.("netxops alarm-push: auth failed (%s)", msg.error); + socket?.close(); + return; + } + if (type === "pong") + return; + if (type === "event" && String(msg.event ?? "") === "netx.alarm") { + const payload = msg.payload && typeof msg.payload === "object" ? msg.payload : {}; + Promise.resolve(options.onAlarm(payload)).catch((error) => { + log.warn?.("netxops alarm-push: handler failed:", error); + }); + } + }); + socket.addEventListener("close", () => { + socket = null; + if (!closed) + scheduleReconnect(); + }); + socket.addEventListener("error", () => {}); + }; + const pingTimer = setInterval(() => { + if (socket && socket.readyState === WS.OPEN) { + try { + socket.send(JSON.stringify({ type: "ping", ts: new Date().toISOString() })); + } catch {} + } + }, 25000); + connect(); + return () => { + closed = true; + clearReconnect(); + clearInterval(pingTimer); + try { + socket?.close(); + } catch {} + socket = null; + }; +} + +// src/netx/alarm-session.ts +import { homedir } from "node:os"; +import { join } from "node:path"; +var PRESET_ID = "netxops"; +var PERMISSION_PRESET = "default"; +var TITLE = "Netx 关键告警"; +var sticky = null; +function resolveWorkspacePath() { + const fromEnv = process.env.DSH_HOME?.trim(); + const home = fromEnv && fromEnv.length > 0 ? fromEnv : join(homedir(), ".dsh"); + return join(home, "workspaces", "netxops-alarms"); +} +async function deliverAlarmToSession(ctx, payload, lang = "zh") { + const prompt = formatAlarmPrompt(payload, lang); + const agents = ctx.agents; + if (!agents || typeof agents.create !== "function") { + ctx.logger.warn("netxops alarm-push: ctx.agents unavailable — enable a profile that mounts agents to receive alarms in a DSH session"); + return; + } + if (sticky?.agent && typeof sticky.agent.followup === "function") { + try { + await followup(ctx, sticky.agent, prompt); + return; + } catch (error) { + ctx.logger.warn("netxops alarm-push: sticky followup failed, recreating session: %s", error); + sticky = null; + } + } + await createStickySession(ctx, prompt); +} +async function followup(ctx, agent, prompt) { + let createUserMessage; + try { + const mod = await import("@deepseek-ai/dsh-llm"); + createUserMessage = mod.createUserMessage; + if (typeof createUserMessage !== "function") + throw new Error("createUserMessage missing"); + const summary = typeof mod.boundContextSummary === "function" ? mod.boundContextSummary("netx key alarm") : "netx key alarm"; + agent.followup(createUserMessage({ + content: [{ type: "text", text: prompt }], + source: { + kind: "webhook", + provider: "netx", + source: "dsh-alarm-hub", + form: "notice", + summary + } + })); + } catch (error) { + const anyAgent = agent; + if (typeof anyAgent.prompt === "function") { + await anyAgent.prompt(prompt); + return; + } + ctx.logger.warn("netxops alarm-push: cannot build user message (%s)", error); + throw error; + } +} +async function createStickySession(ctx, prompt) { + const c = ctx; + const agents = c.agents; + const agentPresets = c.agentPresets; + const workspaceRegistry = c.workspaceRegistry; + const permissionPresets = c.permissionPresets; + const sessionTitle = c.sessionTitle; + const agentDefaultModel = c.agentDefaultModel; + if (!agentPresets || typeof agentPresets.resolve !== "function" || !workspaceRegistry || typeof workspaceRegistry.create !== "function") { + ctx.logger.warn("netxops alarm-push: agentPresets/workspaceRegistry unavailable — cannot create a DSH session"); + return; + } + if (permissionPresets && typeof permissionPresets.resolve === "function") { + try { + permissionPresets.resolve(PERMISSION_PRESET); + } catch { + ctx.logger.warn("netxops alarm-push: permission preset %s missing", PERMISSION_PRESET); + } + } + const preset = await agentPresets.resolve(PRESET_ID); + if (typeof agentPresets.standingKeyFor === "function") { + await agentPresets.standingKeyFor(preset.id); + } + const selected = typeof agentDefaultModel?.currentSelection === "function" ? agentDefaultModel.currentSelection() : { provider: "deepseek", model: "deepseek-chat" }; + const workspacePath = resolveWorkspacePath(); + const workspace = await workspaceRegistry.create(workspacePath); + const sessionId = `netxops-alarm-${Date.now().toString(36)}`; + const handle = await agents.create({ + sessionId, + meta: { cwd: workspace.path, agentPreset: preset.id }, + agentOptions: { provider: selected.provider, model: selected.model }, + setup: async (agentCtx) => { + if (typeof agentPresets.mount === "function") { + await agentPresets.mount(agentCtx, preset.id); + } + } + }); + try { + if (typeof workspace.attachSession === "function") { + await workspace.attachSession(sessionId); + } + if (permissionPresets && typeof permissionPresets.set === "function") { + permissionPresets.set(handle.agent.session, PERMISSION_PRESET); + } + if (sessionTitle && typeof sessionTitle.rename === "function") { + sessionTitle.rename(handle.agent.session, TITLE); + } + await followup(ctx, handle.agent, prompt); + sticky = { sessionId, agent: handle.agent }; + ctx.logger.info("netxops alarm-push: opened sticky session %s", sessionId); + } catch (error) { + try { + await handle.dispose?.(); + } catch {} + throw error; + } +} +function resetAlarmSession() { + sticky = null; +} + // src/netx/runtime.ts var STORE_KEY = Symbol.for("dsh-netxops.connection-store"); function store() { @@ -36,33 +325,34 @@ var Config = z.object({ lang: z.string().default("zh"), tokenCredentialRef: z.string().role("credential-ref").default(DEFAULT_TOKEN_REF), toolCallTimeoutMs: z.number().step(1).min(1000).default(120000), - installAgentPreset: z.boolean().default(true) + installAgentPreset: z.boolean().default(true), + alarmPushEnabled: z.boolean().default(false) }); function packageRoot() { - return join(dirname(fileURLToPath(import.meta.url)), ".."); + return join2(dirname(fileURLToPath(import.meta.url)), ".."); } function resolveDshHome() { const fromEnv = process.env.DSH_HOME?.trim(); if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv; - return join(homedir(), ".dsh"); + return join2(homedir2(), ".dsh"); } function ensureAgentPresetInstalled(logger) { - const src = join(packageRoot(), "presets", NETXOPS_PRESET_ID); - const composition = join(src, "agent.cordis.yml"); + const src = join2(packageRoot(), "presets", NETXOPS_PRESET_ID); + const composition = join2(src, "agent.cordis.yml"); if (!existsSync(composition)) { logger.warn("netxops: bundled preset missing at %s — skip user-preset install", src); return; } - const destParent = join(resolveDshHome(), ".agent-presets"); - const dest = join(destParent, NETXOPS_PRESET_ID); + const destParent = join2(resolveDshHome(), ".agent-presets"); + const dest = join2(destParent, NETXOPS_PRESET_ID); try { mkdirSync(destParent, { recursive: true }); if (existsSync(dest)) { rmSync(dest, { recursive: true, force: true }); } cpSync(src, dest, { recursive: true }); - writeFileSync(join(dest, ".dsh-netxops-managed"), `${new Date().toISOString()} + writeFileSync(join2(dest, ".dsh-netxops-managed"), `${new Date().toISOString()} `, "utf8"); logger.info("netxops: agent preset installed at %s", dest); } catch (error) { @@ -87,9 +377,27 @@ function apply(ctx, config = Config({})) { let source = () => config; let publishing = Promise.resolve(); let generation = 0; + let stopAlarmPush; if (config.installAgentPreset) { ensureAgentPresetInstalled(ctx.logger); } + const restartAlarmPush = (apiUrl, token, enabled, lang) => { + stopAlarmPush?.(); + stopAlarmPush = undefined; + resetAlarmSession(); + if (!enabled) + return; + if (!token.trim()) { + ctx.logger.warn("netxops alarm-push: enabled but token is empty — not connecting"); + return; + } + stopAlarmPush = startAlarmPushClient({ + apiUrl, + token, + logger: ctx.logger, + onAlarm: (payload) => deliverAlarmToSession(ctx, payload, lang) + }); + }; const publish = () => { publishing = publishing.then(async () => { const gen = ++generation; @@ -105,10 +413,11 @@ function apply(ctx, config = Config({})) { lang: current.lang, toolCallTimeoutMs: current.toolCallTimeoutMs }); + restartAlarmPush(apiUrl, token, current.alarmPushEnabled === true, current.lang); 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", apiUrl); + ctx.logger.info("netxops: published connection → %s tokenConfigured=true alarmPush=%s", apiUrl, current.alarmPushEnabled === true); } }).catch((error) => { ctx.logger.error("netxops: connection publish error: %s", error); @@ -129,6 +438,9 @@ function apply(ctx, config = Config({})) { }); ctx.effect(() => () => { generation += 1; + stopAlarmPush?.(); + stopAlarmPush = undefined; + resetAlarmSession(); }, "netxops: dispose host bridge"); } export { diff --git a/package.json b/package.json index 38ac26d..bd94729 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dsh-netxops", - "version": "0.1.14", - "description": "DeepSeek Harness Netx Ops: Ops-scoped netx__* REST tools + Plugins settings card + agent preset", + "version": "0.1.15", + "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", "private": false, diff --git a/scripts/build-host.mjs b/scripts/build-host.mjs index b43ee3a..e60e6ff 100644 --- a/scripts/build-host.mjs +++ b/scripts/build-host.mjs @@ -16,6 +16,15 @@ const external = [ '@deepseek-ai/dsh-credentials', '@deepseek-ai/dsh-settings', '@deepseek-ai/dsh-tools', + '@deepseek-ai/dsh-llm', + '@deepseek-ai/dsh-brand', + '@deepseek-ai/dsh-agent', + '@deepseek-ai/dsh-agent-presets', + '@deepseek-ai/dsh-workspace', + '@deepseek-ai/dsh-session', + '@deepseek-ai/dsh-session-title', + '@deepseek-ai/dsh-permission-presets', + '@deepseek-ai/dsh-agent-default-model', ] const entries = [ diff --git a/src/client/NetxopsCard.tsx b/src/client/NetxopsCard.tsx index f4f7000..4b645e6 100644 --- a/src/client/NetxopsCard.tsx +++ b/src/client/NetxopsCard.tsx @@ -147,6 +147,38 @@ export function NetxopsCard(props: NetxopsCardProps) { onEdit={(text) => { props.edit('lang', text) }} onReset={() => { props.resetField('lang') }} /> +
+
+ + {state.alarmPushEnabled.overridden + ? ( + + {t('overridden')} + + + ) + : null} +
+ +
{state.failed ?

{t('saveFailed')}

: null}