diff --git a/README.md b/README.md index 9bcf1a8..7a7ad15 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ Then: 1. Settings → **Plugins** → **Netx Ops** → API URL / token / capability groups 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** +3. Optional: **导出全部会话** — download one ZIP of every durable session on this Host. +4. Settings → **Agent presets** → Custom → **Netx Ops** (installed automatically on first host activate) +5. New session → choose **Netx Ops** Also need: a reachable **netx API** (no local `pip install netx_mcp`). @@ -35,6 +36,7 @@ Also need: a reachable **netx API** (no local `pip install netx_mcp`). |---------------|---------------------------| | `netx__*` REST tools (groups: ops / topology; one group ↔ one skill; optional host publish + `tools-ops|topology`) + settings + credentials | netx HTTP API (URL + token) | | Optional key-alarm push (WSS client → sticky DSH session) | netx `/v1/integrations/dsh-alarm/ws` hub | +| **Export all sessions** (Plugins card → browser ZIP download) | this Host’s session store (JSONL) | | Persona + skills (`netx-ops` / `netx-topology`) | | | Agent preset auto-install to `~/.dsh/.agent-presets` | | diff --git a/cordis.patch.yml b/cordis.patch.yml index 4ab3163..f3653b6 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -25,6 +25,7 @@ alarmDeliverIm: false imBotId: '' imTargetId: '' + imTargets: '' groupOpsInPreset: true groupOpsPublic: false groupTopologyInPreset: false diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 4e75a20..95e7b96 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -37,6 +37,7 @@ One command, three **direct** profile bundles. Do **not** nest IM/cron under net 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):///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. + Optional: **导出全部会话** downloads `dsh-sessions--.zip` via `GET /api/netxops.sessions.export` (browser download; works for cloud Hosts). Capability groups: leave default for **ops**, or enable **topology** / **对其他预设公开** (new sessions after save). 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 / 「能否登录」. @@ -48,12 +49,20 @@ One command, three **direct** profile bundles. Do **not** nest IM/cron under net - 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 **optional** for the DSH session path; required only when IM delivery is checked. +## Export all sessions (HQ pickup) + +- **Where:** Settings → Plugins → **Netx Ops** → **导出全部会话**. +- **What:** browser downloads one ZIP — `manifest.json` plus `sessions//session.jsonl` from this Host’s `sessionPersistence`. +- **Why:** each edge / cloud harness keeps its own sessions; Ops downloads from the Web UI and ships archives to HQ. +- **Limits:** needs a JSONL (raw-artifact) persistence backend; SQLite backends refuse export. + ## Verify 1. Plugins card **Netx Ops** visible. 2. With key-alarm push on: card badge shows **Connected** (or reconnecting / auth failed with detail). 3. Agent presets → Custom → **Netx Ops**. 4. Tools include `netx__queryNmsAlarms` / `netx__execManagedNe` / `netx__findTopologyPaths` (**ops** → skill `netx-ops`). Canvas / dual_unit need **topology** (`netx-topology`). +5. Export section shows a session count; **下载 ZIP** starts a browser download. See [TOOL_MAP.md](TOOL_MAP.md). diff --git a/lib/client.js b/lib/client.js index 024ec25..3db25e4 100644 --- a/lib/client.js +++ b/lib/client.js @@ -167,11 +167,79 @@ async function fetchImDeliveryCatalog(call, signal) { function imCatalogOptionKey(botId, targetId) { return `${botId}::${targetId}`; } -function parseImCatalogOptionKey(value) { - const at = value.indexOf("::"); - if (at <= 0) - return { botId: "", targetId: "" }; - return { botId: value.slice(0, at), targetId: value.slice(at + 2) }; + +// src/netx/im-targets.ts +function imTargetKey(botId, targetId) { + return `${botId}::${targetId}`; +} +function normalizeImTarget(input) { + if (input === null || typeof input !== "object" || Array.isArray(input)) + return null; + const row = input; + const botId = typeof row.botId === "string" ? row.botId.trim() : ""; + const targetId = typeof row.targetId === "string" ? row.targetId.trim() : ""; + if (!botId || !targetId) + return null; + return { botId, targetId }; +} +function parseImTargetsJson(text) { + const trimmed = text.trim(); + if (!trimmed) + return []; + let parsed; + try { + parsed = JSON.parse(trimmed); + } catch { + return []; + } + if (!Array.isArray(parsed)) + return []; + const out = []; + const seen = new Set; + for (const entry of parsed) { + const target = normalizeImTarget(entry); + if (!target) + continue; + const key = imTargetKey(target.botId, target.targetId); + if (seen.has(key)) + continue; + seen.add(key); + out.push(target); + } + return out; +} +function formatImTargetsJson(targets) { + const seen = new Set; + const rows = []; + for (const entry of targets) { + const target = normalizeImTarget(entry); + if (!target) + continue; + const key = imTargetKey(target.botId, target.targetId); + if (seen.has(key)) + continue; + seen.add(key); + rows.push(target); + } + return rows.length === 0 ? "" : JSON.stringify(rows); +} +function resolveImTargets(settings) { + const fromList = parseImTargetsJson(typeof settings.imTargets === "string" ? settings.imTargets : ""); + if (fromList.length > 0) + return fromList; + const legacy = normalizeImTarget({ + botId: settings.imBotId, + targetId: settings.imTargetId + }); + return legacy ? [legacy] : []; +} +function setImTargetSelected(current, target, selected) { + const key = imTargetKey(target.botId, target.targetId); + const without = current.filter((row) => imTargetKey(row.botId, row.targetId) !== key); + if (!selected) + return without; + const normalized = normalizeImTarget(target); + return normalized ? [...without, normalized] : without; } // src/client/styles.ts @@ -213,7 +281,11 @@ var CSS = ` .dsh-nx-groupBlock{display:flex;flex-direction:column;gap:8px;padding:8px 0 4px} .dsh-nx-groupTitle{font-size:12px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary)} .dsh-nx-groupChecks{display:flex;flex-direction:column;gap:6px;padding-left:2px} +.dsh-nx-imTargetList{display:flex;flex-direction:column;gap:8px;padding:4px 0 2px} .dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)} +.dsh-nx-exportRow{display:flex;align-items:center;gap:8px;padding-top:4px} +.dsh-nx-export{border-color:var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-primary)} +.dsh-nx-export:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)} .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)} .dsh-nx-btn{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer} @@ -315,97 +387,79 @@ function ValueField(props) { } function ImDeliveryPicker(props) { const options = props.catalog.options; - const selectedKey = props.botId && props.targetId ? imCatalogOptionKey(props.botId, props.targetId) : ""; - const matched = options.some((row) => imCatalogOptionKey(row.botId, row.targetId) === selectedKey); - const [manual, setManual] = import_react.useState(() => !!(selectedKey && !matched)); - import_react.useEffect(() => { - if (selectedKey && matched) - setManual(false); - else if (selectedKey && !matched) - setManual(true); - }, [selectedKey, matched]); - const selectValue = manual ? "__manual__" : matched ? selectedKey : ""; - const showManual = manual || selectedKey && !matched; - return /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, { + const selected = resolveImTargets({ + imTargets: props.targetsJson, + imBotId: props.legacyBotId, + imTargetId: props.legacyTargetId + }); + const selectedKeys = new Set(selected.map((row) => imTargetKey(row.botId, row.targetId))); + const orphanSelected = selected.filter((row) => !options.some((opt) => imCatalogOptionKey(opt.botId, opt.targetId) === imTargetKey(row.botId, row.targetId))); + return /* @__PURE__ */ jsx_runtime.jsxs("div", { + className: "dsh-nx-field", children: [ /* @__PURE__ */ jsx_runtime.jsxs("div", { - className: "dsh-nx-field", + className: "dsh-nx-fieldHead", children: [ - /* @__PURE__ */ jsx_runtime.jsx("div", { - className: "dsh-nx-fieldHead", - children: /* @__PURE__ */ jsx_runtime.jsx("label", { - className: "dsh-nx-label", - htmlFor: "netxops-im-target", - children: props.labels.target - }) + /* @__PURE__ */ jsx_runtime.jsx("span", { + className: "dsh-nx-label", + children: props.labels.target }), - /* @__PURE__ */ jsx_runtime.jsxs("select", { - id: "netxops-im-target", - className: "dsh-nx-input dsh-nx-select", - value: selectValue, - disabled: props.disabled, - onChange: (event) => { - const value = event.target.value; - if (value === "__manual__") { - setManual(true); - return; - } - setManual(false); - if (!value) { - props.onPick("", ""); - return; - } - const next = parseImCatalogOptionKey(value); - props.onPick(next.botId, next.targetId); - }, - children: [ - /* @__PURE__ */ jsx_runtime.jsx("option", { - value: "", - children: options.length === 0 ? props.labels.none : `— ${props.labels.target} —` - }), - options.map((row) => /* @__PURE__ */ jsx_runtime.jsx("option", { - value: imCatalogOptionKey(row.botId, row.targetId), - children: `${row.name} · ${row.channel || "im"} · ${row.targetId}` - }, imCatalogOptionKey(row.botId, row.targetId))), - /* @__PURE__ */ jsx_runtime.jsx("option", { - value: "__manual__", - children: props.labels.manual - }) - ] - }), - !props.catalog.available || options.length === 0 ? /* @__PURE__ */ jsx_runtime.jsx("p", { - className: "dsh-nx-hint", - children: props.catalog.hint || props.labels.unavailable + selected.length > 0 ? /* @__PURE__ */ jsx_runtime.jsx("span", { + className: "dsh-nx-badgeMuted", + children: props.labels.selectedCount.replace("{count}", String(selected.length)) }) : null ] }), - showManual ? /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, { + options.length === 0 && orphanSelected.length === 0 ? /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-hint", + children: !props.catalog.available ? props.catalog.hint || props.labels.unavailable : props.labels.none + }) : /* @__PURE__ */ jsx_runtime.jsxs("div", { + className: "dsh-nx-imTargetList", children: [ - /* @__PURE__ */ jsx_runtime.jsx(ValueField, { - id: "netxops-im-bot-id", - label: props.labels.botId, - hint: props.labels.botHint, - field: props.botField, - overriddenLabel: props.labels.overridden, - resetLabel: props.labels.reset, - invalidLabel: props.labels.invalid, - disabled: props.disabled, - onEdit: props.onEditBot, - onReset: props.onResetBot + options.map((row) => { + const key = imCatalogOptionKey(row.botId, row.targetId); + const checked = selectedKeys.has(key); + return /* @__PURE__ */ jsx_runtime.jsxs("label", { + className: "dsh-nx-checkRow", + children: [ + /* @__PURE__ */ jsx_runtime.jsx("input", { + type: "checkbox", + checked, + disabled: props.disabled, + onChange: (event) => { + props.onChange(setImTargetSelected(selected, { botId: row.botId, targetId: row.targetId }, event.target.checked)); + } + }), + /* @__PURE__ */ jsx_runtime.jsx("span", { + children: `${row.name} · ${row.channel || "im"} · ${row.targetId}` + }) + ] + }, key); }), - /* @__PURE__ */ jsx_runtime.jsx(ValueField, { - id: "netxops-im-target-id", - label: props.labels.targetId, - hint: props.labels.targetHint, - field: props.targetField, - overriddenLabel: props.labels.overridden, - resetLabel: props.labels.reset, - invalidLabel: props.labels.invalid, - disabled: props.disabled, - onEdit: props.onEditTarget, - onReset: props.onResetTarget + orphanSelected.map((row) => { + const key = imTargetKey(row.botId, row.targetId); + return /* @__PURE__ */ jsx_runtime.jsxs("label", { + className: "dsh-nx-checkRow", + children: [ + /* @__PURE__ */ jsx_runtime.jsx("input", { + type: "checkbox", + checked: true, + disabled: props.disabled, + onChange: (event) => { + props.onChange(setImTargetSelected(selected, row, event.target.checked)); + } + }), + /* @__PURE__ */ jsx_runtime.jsx("span", { + children: `${row.botId} · ${row.targetId}` + }) + ] + }, `orphan-${key}`); }) ] + }), + !props.catalog.available && options.length > 0 ? /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-hint", + children: props.catalog.hint || props.labels.unavailable }) : null ] }); @@ -625,10 +679,6 @@ function NetxopsCard(props) { children: t("capabilityGroups") }) }), - /* @__PURE__ */ jsx_runtime.jsx("p", { - className: "dsh-nx-hint", - children: t("capabilityGroupsHint") - }), /* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, { title: t("groupOps"), inPresetLabel: t("groupInPreset"), @@ -698,23 +748,19 @@ function NetxopsCard(props) { }) ] }), - /* @__PURE__ */ jsx_runtime.jsxs("label", { + /* @__PURE__ */ jsx_runtime.jsx("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") - }) - ] + children: /* @__PURE__ */ jsx_runtime.jsx("input", { + id: "netxops-alarm-push", + type: "checkbox", + checked: state.alarmPushEnabled.text === "true", + disabled, + "aria-label": t("alarmPushEnabled"), + onChange: (event) => { + props.edit("alarmPushEnabled", event.target.checked ? "true" : "false"); + } + }) }), pushStatus?.wsUrl ? /* @__PURE__ */ jsx_runtime.jsx("p", { className: "dsh-nx-hint", @@ -758,114 +804,87 @@ function NetxopsCard(props) { }) : null ] }), - /* @__PURE__ */ jsx_runtime.jsxs("label", { + /* @__PURE__ */ jsx_runtime.jsx("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") - }) - ] + children: /* @__PURE__ */ jsx_runtime.jsx("input", { + id: "netxops-alarm-dsh", + type: "checkbox", + checked: state.alarmDeliverDsh.text === "true", + disabled, + "aria-label": t("alarmDeliverDsh"), + onChange: (event) => { + props.edit("alarmDeliverDsh", event.target.checked ? "true" : "false"); + } + }) }) ] }), /* @__PURE__ */ jsx_runtime.jsx(ImDeliveryPicker, { catalog: state.imDeliveryCatalog, - botId: state.imBotId.text, - targetId: state.imTargetId.text, + targetsJson: state.imTargets.text, + legacyBotId: state.imBotId.text, + legacyTargetId: state.imTargetId.text, disabled, labels: { - target: t("imTarget"), + target: t("alarmDeliverIm"), none: t("imTargetNone"), - manual: t("imTargetManual"), - unavailable: t("imCatalogUnavailable"), - botId: t("imBotId"), - botHint: t("imBotIdHint"), - targetId: t("imTargetId"), - targetHint: t("imTargetIdHint"), - overridden: t("overridden"), - reset: t("reset"), - invalid: t("invalid") + selectedCount: t("imTargetSelectedCount"), + unavailable: t("imCatalogUnavailable") }, - botField: state.imBotId, - targetField: state.imTargetId, - onPick: (botId, targetId) => { - props.edit("imBotId", botId); - props.edit("imTargetId", targetId); - }, - onEditBot: (text) => { - props.edit("imBotId", text); - }, - onEditTarget: (text) => { - props.edit("imTargetId", text); - }, - onResetBot: () => { - props.resetField("imBotId"); - }, - onResetTarget: () => { - props.resetField("imTargetId"); + onChange: (targets) => { + props.edit("imTargets", formatImTargetsJson(targets)); + const first = targets[0]; + props.edit("imBotId", first?.botId ?? ""); + props.edit("imTargetId", first?.targetId ?? ""); + props.edit("alarmDeliverIm", targets.length > 0 ? "true" : "false"); } }), + /* @__PURE__ */ jsx_runtime.jsxs("div", { + className: "dsh-nx-field", + children: [ + /* @__PURE__ */ jsx_runtime.jsx("div", { + className: "dsh-nx-fieldHead", + children: /* @__PURE__ */ jsx_runtime.jsx("span", { + className: "dsh-nx-label", + children: t("sessionsExport") + }) + }), + state.sessionsExportStatus === null ? /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-hint", + children: t("sessionsExportUnavailable") + }) : state.sessionsExportStatus.available ? /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-hint", + children: t("sessionsExportCount").replace("{count}", String(state.sessionsExportStatus.sessionCount)) + }) : /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-invalid", + role: "status", + children: state.sessionsExportStatus.reason || t("sessionsExportUnavailable") + }), + state.sessionsExportError ? /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-invalid", + role: "status", + children: state.sessionsExportError + }) : null, + state.sessionsExportLastFile && !state.sessionsExportBusy && !state.sessionsExportError ? /* @__PURE__ */ jsx_runtime.jsx("p", { + className: "dsh-nx-hint", + role: "status", + children: t("sessionsExportDone").replace("{file}", state.sessionsExportLastFile) + }) : null, + /* @__PURE__ */ jsx_runtime.jsx("div", { + className: "dsh-nx-exportRow", + children: /* @__PURE__ */ jsx_runtime.jsx("button", { + type: "button", + className: "dsh-nx-btn dsh-nx-export", + disabled: state.sessionsExportBusy || state.sessionsExportStatus?.available !== true, + onClick: () => { + props.exportAllSessions(); + }, + children: t(state.sessionsExportBusy ? "sessionsExportBusy" : "sessionsExportButton") + }) + }) + ] + }), /* @__PURE__ */ jsx_runtime.jsxs("div", { className: "dsh-nx-footer", children: [ @@ -1118,6 +1137,91 @@ class CardForm { } } +// src/session-export-shared.ts +var NETXOPS_SESSIONS_EXPORT_PATH = "/api/netxops.sessions.export"; + +// src/client/sessions-export-view.ts +var SESSIONS_EXPORT_STATUS_ENDPOINT = "sessions.export.status"; +var EMPTY_STATUS = { + available: false, + sessionCount: 0, + supportsRawArtifacts: false, + reason: "RPC unavailable" +}; +function asSessionsExportStatus(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return { ...EMPTY_STATUS }; + } + const row = value; + return { + available: row.available === true, + sessionCount: typeof row.sessionCount === "number" ? row.sessionCount : 0, + supportsRawArtifacts: row.supportsRawArtifacts === true, + reason: typeof row.reason === "string" ? row.reason : undefined + }; +} +async function fetchSessionsExportStatus(call, signal) { + const result = await call(NETXOPS_RPC_CHANNEL, SESSIONS_EXPORT_STATUS_ENDPOINT, {}, signal); + if (result !== null && typeof result === "object" && result.ok === true) { + return asSessionsExportStatus(result.value); + } + if (result !== null && typeof result === "object" && result.ok === false) { + return { + ...EMPTY_STATUS, + reason: String(result.error?.message ?? "rpc failed") + }; + } + return asSessionsExportStatus(result); +} +function hostBase() { + const origin = globalThis.location?.origin; + return origin !== undefined && origin !== "null" ? origin : "http://dsh.internal"; +} +function saveBlobDownload(blob, filename) { + const objectUrl = URL.createObjectURL(blob); + try { + const anchor = document.createElement("a"); + anchor.href = objectUrl; + anchor.download = filename; + anchor.rel = "noopener"; + anchor.style.display = "none"; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + } finally { + globalThis.setTimeout(() => { + URL.revokeObjectURL(objectUrl); + }, 60000); + } +} +async function downloadAllSessionsExport(fetcher = fetch, save = saveBlobDownload) { + const url = new URL(NETXOPS_SESSIONS_EXPORT_PATH, hostBase()); + const response = await fetcher(url, { + method: "GET", + credentials: "include" + }); + if (!response.ok) { + const fromHeader = response.headers.get("x-netxops-export-error") ?? ""; + const detail = fromHeader || await response.text().catch(() => ""); + throw new Error(`Export failed: HTTP ${response.status}${detail === "" ? "" : ` ${detail}`}`); + } + const disposition = response.headers.get("content-disposition") ?? ""; + const matched = /filename="([^"]+)"/i.exec(disposition); + const filename = matched?.[1] && matched[1].length > 0 ? matched[1] : "dsh-sessions-export.zip"; + const countHeader = response.headers.get("x-netxops-session-count"); + const sessionCount = countHeader !== null && countHeader !== "" ? Number.parseInt(countHeader, 10) : 0; + const blob = await response.blob(); + if (blob.size <= 0) { + throw new Error("Export failed: empty ZIP body"); + } + save(blob, filename); + return { + filename, + sessionCount: Number.isFinite(sessionCount) ? sessionCount : 0, + bytes: blob.size + }; +} + // src/client/controller.ts var NETXOPS_NS = "netxops"; var DEFAULT_TOKEN_REF = "NETX_API_TOKEN"; @@ -1138,9 +1242,15 @@ class NetxopsCardController { rpcCall; alarmPushStatus = null; imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }; + sessionsExportStatus = null; + sessionsExportBusy = false; + sessionsExportError = null; + sessionsExportLastFile = null; pollTimer; pollInFlight = false; catalogInFlight = false; + exportStatusInFlight = false; + exportInFlight; constructor(scope, ctx) { this.scope = scope; this.ctx = ctx; @@ -1155,6 +1265,7 @@ class NetxopsCardController { booleanField("alarmPushEnabled"), booleanFieldPersistFalse("alarmDeliverDsh"), booleanField("alarmDeliverIm"), + textField("imTargets"), textField("imBotId"), textField("imTargetId") ], [{ field: API_TOKEN_FIELD, write: (text) => this.writeToken(text) }]); @@ -1189,6 +1300,10 @@ class NetxopsCardController { this.imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG }; changed = true; } + if (this.sessionsExportStatus !== null) { + this.sessionsExportStatus = null; + changed = true; + } if (changed) this.store.set(this.projection()); return; @@ -1196,6 +1311,7 @@ class NetxopsCardController { this.startStatusPoll(); this.refreshAlarmPushStatus(); this.refreshImDeliveryCatalog(); + this.refreshSessionsExportStatus(); } startStatusPoll() { if (this.pollTimer !== undefined) @@ -1203,6 +1319,7 @@ class NetxopsCardController { this.pollTimer = setInterval(() => { this.refreshAlarmPushStatus(); this.refreshImDeliveryCatalog(); + this.refreshSessionsExportStatus(); }, STATUS_POLL_MS); } stopStatusPoll() { @@ -1247,6 +1364,42 @@ class NetxopsCardController { this.catalogInFlight = false; } } + async refreshSessionsExportStatus() { + const call = this.rpcCall; + if (call === undefined || this.exportStatusInFlight) + return; + this.exportStatusInFlight = true; + try { + const next = await fetchSessionsExportStatus(call); + const prev = this.sessionsExportStatus; + if (prev && prev.available === next.available && prev.sessionCount === next.sessionCount && prev.supportsRawArtifacts === next.supportsRawArtifacts && prev.reason === next.reason) + return; + this.sessionsExportStatus = next; + this.store.set(this.projection()); + } catch {} finally { + this.exportStatusInFlight = false; + } + } + exportAllSessions() { + if (this.exportInFlight !== undefined || this.sessionsExportBusy) + return; + if (this.sessionsExportStatus?.available !== true) + return; + this.sessionsExportBusy = true; + this.sessionsExportError = null; + this.store.set(this.projection()); + this.exportInFlight = downloadAllSessionsExport().then((result) => { + this.sessionsExportLastFile = result.filename; + this.sessionsExportError = null; + }).catch((error) => { + this.sessionsExportError = error instanceof Error ? error.message : String(error); + }).finally(() => { + this.sessionsExportBusy = false; + this.exportInFlight = undefined; + this.store.set(this.projection()); + this.refreshSessionsExportStatus(); + }); + } projection() { return { ...this.form.shell(), @@ -1260,6 +1413,7 @@ class NetxopsCardController { alarmPushEnabled: this.form.field("alarmPushEnabled"), alarmDeliverDsh: this.form.field("alarmDeliverDsh"), alarmDeliverIm: this.form.field("alarmDeliverIm"), + imTargets: this.form.field("imTargets"), imBotId: this.form.field("imBotId"), imTargetId: this.form.field("imTargetId"), apiToken: this.form.field(API_TOKEN_FIELD), @@ -1267,7 +1421,11 @@ class NetxopsCardController { apiTokenWritable: this.credential.remoteReady && this.credential.writable, apiTokenRemoteReady: this.credential.remoteReady, alarmPushStatus: this.alarmPushStatus, - imDeliveryCatalog: this.imDeliveryCatalog + imDeliveryCatalog: this.imDeliveryCatalog, + sessionsExportStatus: this.sessionsExportStatus, + sessionsExportBusy: this.sessionsExportBusy, + sessionsExportError: this.sessionsExportError, + sessionsExportLastFile: this.sessionsExportLastFile }; } credentials() { @@ -1313,7 +1471,13 @@ class NetxopsCardController { this.readCredential(); } inject() { - return { hooks: { netxopsCard: this.store }, ...this.form.actions() }; + return { + hooks: { netxopsCard: this.store }, + ...this.form.actions(), + exportAllSessions: () => { + this.exportAllSessions(); + } + }; } async writeToken(value) { const api = this.credentials(); @@ -1332,22 +1496,20 @@ function refOf(snapshot) { // src/client/locales.ts var en = { title: "Netx Ops", - description: "netx API, bearer token, and capability groups (ops / topology).", + description: "API, token, capabilities, and alarm delivery.", apiUrl: "API URL", - apiUrlHint: "netx REST root, e.g. http://127.0.0.1:8890 (also used for alarm subscribe)", + apiUrlHint: "e.g. http://127.0.0.1:8890", lang: "Language", - langHint: "Response language hint (zh / en).", + langHint: "zh / en", capabilityGroups: "Capability groups", - capabilityGroupsHint: "One group ↔ one skill. ops = netx-ops (alarms + CLI login + paths); topology = netx-topology. Defaults: ops on; topology and public off. New sessions after save. Other agents: dsh-netxops/tools-ops|topology.", - nmsProvider: "nms provider", - nmsProviderHint: "Vendor adapter id. Supported today: zte-ume (REST still /v1/ume/*; model tools are netx__*Nms*).", - groupOps: "ops — skill netx-ops (NMS alarms/inventory + managed CLI login + paths)", - groupTopology: "topology — skill netx-topology (canvas / fabric / dual_unit)", + nmsProvider: "NMS provider", + nmsProviderHint: "zte-ume", + groupOps: "ops", + groupTopology: "topology", groupInPreset: "In Netx Ops preset", groupPublic: "Publish to other presets", alarmPushEnabled: "Key-alarm push", - alarmPushEnabledHint: "Dial out to netx when a matched key alarm arrives. Choose DSH and/or IM sinks below.", - alarmPushStatus: "Push link", + alarmPushStatus: "Status", alarmPushPhaseDisabled: "Off", alarmPushPhaseIdle: "Idle", alarmPushPhaseConnecting: "Connecting…", @@ -1357,52 +1519,49 @@ var en = { 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 ≥ops.24. Pick a saved delivery target below (create targets under IM → Delivery settings).", - imBotId: "IM Bot ID", - imBotIdHint: "Filled from the picker, or enter manually.", - imTargetId: "IM Target ID", - imTargetIdHint: "Filled from the picker, or enter manually.", - imTarget: "Delivery target", - imTargetNone: "(Create a target in IM delivery settings first)", - imTargetManual: "Enter manually…", - imCatalogUnavailable: "Cannot load targets — install/update dsh-im-ops (≥ops.24) and save at least one delivery target.", + imTargetNone: "No delivery targets yet — create one in IM delivery settings.", + imTargetSelectedCount: "{count} selected", + imCatalogUnavailable: "Cannot load IM targets (install/update dsh-im-ops).", 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.", + apiTokenHint: "Stored as NETX_API_TOKEN. Leave blank to keep current.", + apiTokenUnavailable: "remote.credentials unavailable — set NETX_API_TOKEN via script.", apiTokenSet: "Configured", apiTokenUnset: "Not set", + sessionsExport: "Export all sessions", + sessionsExportCount: "{count} sessions", + sessionsExportUnavailable: "Export unavailable", + sessionsExportButton: "Download ZIP", + sessionsExportBusy: "Preparing…", + sessionsExportDone: "Downloaded: {file}", overridden: "Overridden", reset: "Reset", - invalid: "Invalid value", + invalid: "Invalid", expand: "Expand", collapse: "Collapse", unsaved: "Unsaved", - readOnly: "This document is read-only.", - saveFailed: "Save failed — drafts kept for correction.", + readOnly: "Read-only", + saveFailed: "Save failed", discard: "Discard", save: "Save", saving: "Saving…" }; var zh = { title: "Netx Ops", - description: "netx API、Bearer Token,以及能力组(ops / topology)。", + description: "API、Token、能力组与告警投递。", apiUrl: "API 地址", - apiUrlHint: "netx REST 根地址,例如 http://127.0.0.1:8890(告警订阅复用同一地址)", + apiUrlHint: "例如 http://127.0.0.1:8890", lang: "语言", - langHint: "响应语言提示(zh / en)。", + langHint: "zh / en", capabilityGroups: "能力组", - capabilityGroupsHint: "一组一个 skill。ops = netx-ops(告警 + 纳管登录 + 路径);topology = netx-topology。默认 ops 开;topology / 公开关。保存后新会话生效。其他 Agent:dsh-netxops/tools-ops|topology。", - nmsProvider: "nms provider", - nmsProviderHint: "厂商适配器 id。当前支持:zte-ume(REST 仍为 /v1/ume/*;模型工具名为 netx__*Nms*)。", - groupOps: "ops — skill netx-ops(NMS 告警/库存 + 纳管 CLI 登录 + 路径)", - groupTopology: "topology — skill netx-topology(画布 / Fabric / dual_unit)", + nmsProvider: "NMS provider", + nmsProviderHint: "zte-ume", + groupOps: "ops", + groupTopology: "topology", groupInPreset: "在 Netx Ops 预设中启用", groupPublic: "对其他预设公开", alarmPushEnabled: "关键告警推送", - alarmPushEnabledHint: "主动连接 netx;匹配到关键告警后按下述开关投递。可只开 DSH、只开 IM,或两者都开。", - alarmPushStatus: "推送链路", + alarmPushStatus: "状态", alarmPushPhaseDisabled: "未开启", alarmPushPhaseIdle: "空闲", alarmPushPhaseConnecting: "连接中…", @@ -1412,30 +1571,29 @@ var zh = { alarmPushPhaseAuthFailed: "鉴权失败", alarmPushPhaseError: "异常", alarmDeliverDsh: "投递到 DSH 会话", - alarmDeliverDshHint: "在本机打开/续写 sticky「Netx 关键告警」会话。", alarmDeliverIm: "投递到 WhatsApp / IM", - alarmDeliverImHint: "需要 dsh-im-ops ≥ops.24。请在下方下拉选择已保存的投递目标(先在 IM「投递设置」新建)。", - imBotId: "IM Bot ID", - imBotIdHint: "由下拉自动填入,也可手动改。", - imTargetId: "IM Target ID", - imTargetIdHint: "由下拉自动填入,也可手动改。", - imTarget: "投递目标", - imTargetNone: "(请先在 IM「投递设置」新建目标)", - imTargetManual: "手动填写…", - imCatalogUnavailable: "无法加载投递目标 — 请安装/更新 dsh-im-ops(≥ops.24)并至少保存一个投递目标。", + imTargetNone: "暂无投递目标 — 请先在 IM「投递设置」新建。", + imTargetSelectedCount: "已选 {count} 个", + imCatalogUnavailable: "无法加载投递目标(请安装/更新 dsh-im-ops)。", apiToken: "API Token", - apiTokenHint: "写入凭据 NETX_API_TOKEN(不会进 settings)。留空表示保留已有 token。", - apiTokenUnavailable: "当前 DSH 未提供 remote.credentials。请用 scripts/set-netx-token.ps1(或 .sh)写入 token;若 harness 在监视凭据文件则无需重启。", + apiTokenHint: "写入凭据 NETX_API_TOKEN;留空表示保留已有。", + apiTokenUnavailable: "未提供 remote.credentials — 请用脚本写入 NETX_API_TOKEN。", apiTokenSet: "已配置", apiTokenUnset: "未设置", + sessionsExport: "导出全部会话", + sessionsExportCount: "{count} 个会话", + sessionsExportUnavailable: "无法导出", + sessionsExportButton: "下载 ZIP", + sessionsExportBusy: "准备中…", + sessionsExportDone: "已下载:{file}", overridden: "已覆盖", reset: "重置", - invalid: "无效值", + invalid: "无效", expand: "展开", collapse: "收起", unsaved: "未保存", - readOnly: "当前文档只读。", - saveFailed: "保存失败,草稿已保留以便修改。", + readOnly: "只读", + saveFailed: "保存失败", discard: "丢弃", save: "保存", saving: "保存中…" diff --git a/lib/index.js b/lib/index.js index c1bea4b..6a13d46 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,7 +3,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url); // src/index.ts import { cpSync, existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { homedir as homedir2 } from "node:os"; +import { homedir as homedir3 } from "node:os"; import { dirname as dirname2, join as join3 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; import z from "@deepseek-ai/schemastery"; @@ -250,14 +250,86 @@ function startAlarmPushClient(options) { }; } +// src/netx/im-targets.ts +function imTargetKey(botId, targetId) { + return `${botId}::${targetId}`; +} +function normalizeImTarget(input) { + if (input === null || typeof input !== "object" || Array.isArray(input)) + return null; + const row = input; + const botId = typeof row.botId === "string" ? row.botId.trim() : ""; + const targetId = typeof row.targetId === "string" ? row.targetId.trim() : ""; + if (!botId || !targetId) + return null; + return { botId, targetId }; +} +function parseImTargetsJson(text) { + const trimmed = text.trim(); + if (!trimmed) + return []; + let parsed; + try { + parsed = JSON.parse(trimmed); + } catch { + return []; + } + if (!Array.isArray(parsed)) + return []; + const out = []; + const seen = new Set; + for (const entry of parsed) { + const target = normalizeImTarget(entry); + if (!target) + continue; + const key = imTargetKey(target.botId, target.targetId); + if (seen.has(key)) + continue; + seen.add(key); + out.push(target); + } + return out; +} +function resolveImTargets(settings) { + const fromList = parseImTargetsJson(typeof settings.imTargets === "string" ? settings.imTargets : ""); + if (fromList.length > 0) + return fromList; + const legacy = normalizeImTarget({ + botId: settings.imBotId, + targetId: settings.imTargetId + }); + return legacy ? [legacy] : []; +} + // src/netx/alarm-im.ts +function resolveTargets(options) { + if (options.targets && options.targets.length > 0) { + const out = []; + const seen = new Set; + for (const entry of options.targets) { + const target = normalizeImTarget(entry); + if (!target) + continue; + const key = `${target.botId}::${target.targetId}`; + if (seen.has(key)) + continue; + seen.add(key); + out.push(target); + } + return out; + } + return resolveImTargets({ + imTargets: options.imTargets, + imBotId: options.botId, + imTargetId: options.targetId + }); +} 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)"); + const targets = resolveTargets(options); + if (targets.length === 0) { + ctx.logger.warn("netxops alarm-im: enabled but no delivery targets — skip IM delivery (pick one or more targets under Netx Ops → IM)"); return; } const im = (typeof ctx.get === "function" ? ctx.get("dshIm") : undefined) ?? ctx.dshIm; @@ -266,12 +338,15 @@ async function deliverAlarmToIm(ctx, payload, options) { 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); - } + const results = await Promise.allSettled(targets.map((target) => im.send(target.botId, target.targetId, text))); + results.forEach((result, index) => { + const target = targets[index]; + if (result.status === "fulfilled") { + ctx.logger.info("netxops alarm-im: sent to botId=%s targetId=%s", target.botId, target.targetId); + return; + } + ctx.logger.warn("netxops alarm-im: send failed botId=%s targetId=%s: %s", target.botId, target.targetId, result.reason); + }); } // src/netx/alarm-session.ts @@ -660,6 +735,1080 @@ function watchNetxConnection(listener) { }; } +// src/netx/session-export.ts +import { homedir as homedir2, hostname as osHostname } from "node:os"; + +// node_modules/fflate/esm/index.mjs +import { createRequire as createRequire2 } from "module"; +var require2 = createRequire2("/"); +var _a; +var Worker; +var isMarkedAsUntransferable; +try { + _a = require2("worker_threads"), Worker = _a.Worker, isMarkedAsUntransferable = _a.isMarkedAsUntransferable; +} catch (e) {} +var u8 = Uint8Array; +var u16 = Uint16Array; +var i32 = Int32Array; +var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 0, 0, 0]); +var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 0, 0]); +var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); +var freb = function(eb, start) { + var b = new u16(31); + for (var i = 0;i < 31; ++i) { + b[i] = start += 1 << eb[i - 1]; + } + var r = new i32(b[30]); + for (var i = 1;i < 30; ++i) { + for (var j = b[i];j < b[i + 1]; ++j) { + r[j] = j - b[i] << 5 | i; + } + } + return { b, r }; +}; +var _a = freb(fleb, 2); +var fl = _a.b; +var revfl = _a.r; +fl[28] = 258, revfl[258] = 28; +var _b = freb(fdeb, 0); +var fd = _b.b; +var revfd = _b.r; +var rev = new u16(32768); +for (i = 0;i < 32768; ++i) { + x = (i & 43690) >> 1 | (i & 21845) << 1; + x = (x & 52428) >> 2 | (x & 13107) << 2; + x = (x & 61680) >> 4 | (x & 3855) << 4; + rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1; +} +var x; +var i; +var hMap = function(cd, mb, r) { + var s = cd.length; + var i2 = 0; + var l = new u16(mb); + for (;i2 < s; ++i2) { + if (cd[i2]) + ++l[cd[i2] - 1]; + } + var le = new u16(mb); + for (i2 = 1;i2 < mb; ++i2) { + le[i2] = le[i2 - 1] + l[i2 - 1] << 1; + } + var co; + if (r) { + co = new u16(1 << mb); + var rvb = 15 - mb; + for (i2 = 0;i2 < s; ++i2) { + if (cd[i2]) { + var sv = i2 << 4 | cd[i2]; + var r_1 = mb - cd[i2]; + var v = le[cd[i2] - 1]++ << r_1; + for (var m = v | (1 << r_1) - 1;v <= m; ++v) { + co[rev[v] >> rvb] = sv; + } + } + } + } else { + co = new u16(s); + for (i2 = 0;i2 < s; ++i2) { + if (cd[i2]) { + co[i2] = rev[le[cd[i2] - 1]++] >> 15 - cd[i2]; + } + } + } + return co; +}; +var flt = new u8(288); +for (i = 0;i < 144; ++i) + flt[i] = 8; +var i; +for (i = 144;i < 256; ++i) + flt[i] = 9; +var i; +for (i = 256;i < 280; ++i) + flt[i] = 7; +var i; +for (i = 280;i < 288; ++i) + flt[i] = 8; +var i; +var fdt = new u8(32); +for (i = 0;i < 32; ++i) + fdt[i] = 5; +var i; +var flm = /* @__PURE__ */ hMap(flt, 9, 0); +var fdm = /* @__PURE__ */ hMap(fdt, 5, 0); +var shft = function(p) { + return (p + 7) / 8 | 0; +}; +var slc = function(v, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v.length) + e = v.length; + return new u8(v.subarray(s, e)); +}; +var ec = [ + "unexpected EOF", + "invalid block type", + "invalid length/literal", + "invalid distance", + "stream finished", + "no stream handler", + , + "no callback", + "invalid UTF-8 data", + "extra field too long", + "date not in range 1980-2099", + "filename too long", + "stream finishing", + "invalid zip data" +]; +var err = function(ind, msg, nt) { + var e = new Error(msg || ec[ind]); + e.code = ind; + if (Error.captureStackTrace) + Error.captureStackTrace(e, err); + if (!nt) + throw e; + return e; +}; +var wbits = function(d, p, v) { + v <<= p & 7; + var o = p / 8 | 0; + d[o] |= v; + d[o + 1] |= v >> 8; +}; +var wbits16 = function(d, p, v) { + v <<= p & 7; + var o = p / 8 | 0; + d[o] |= v; + d[o + 1] |= v >> 8; + d[o + 2] |= v >> 16; +}; +var hTree = function(d, mb) { + var t = []; + for (var i2 = 0;i2 < d.length; ++i2) { + if (d[i2]) + t.push({ s: i2, f: d[i2] }); + } + var s = t.length; + var t2 = t.slice(); + if (!s) + return { t: et, l: 0 }; + if (s == 1) { + var v = new u8(t[0].s + 1); + v[t[0].s] = 1; + return { t: v, l: 1 }; + } + t.sort(function(a, b) { + return a.f - b.f; + }); + t.push({ s: -1, f: 25001 }); + var l = t[0], r = t[1], i0 = 0, i1 = 1, i22 = 2; + t[0] = { s: -1, f: l.f + r.f, l, r }; + while (i1 != s - 1) { + l = t[t[i0].f < t[i22].f ? i0++ : i22++]; + r = t[i0 != i1 && t[i0].f < t[i22].f ? i0++ : i22++]; + t[i1++] = { s: -1, f: l.f + r.f, l, r }; + } + var maxSym = t2[0].s; + for (var i2 = 1;i2 < s; ++i2) { + if (t2[i2].s > maxSym) + maxSym = t2[i2].s; + } + var tr = new u16(maxSym + 1); + var mbt = ln(t[i1 - 1], tr, 0); + if (mbt > mb) { + var i2 = 0, dt = 0; + var lft = mbt - mb, cst = 1 << lft; + t2.sort(function(a, b) { + return tr[b.s] - tr[a.s] || a.f - b.f; + }); + for (;i2 < s; ++i2) { + var i2_1 = t2[i2].s; + if (tr[i2_1] > mb) { + dt += cst - (1 << mbt - tr[i2_1]); + tr[i2_1] = mb; + } else + break; + } + dt >>= lft; + while (dt > 0) { + var i2_2 = t2[i2].s; + if (tr[i2_2] < mb) + dt -= 1 << mb - tr[i2_2]++ - 1; + else + ++i2; + } + for (;i2 >= 0 && dt; --i2) { + var i2_3 = t2[i2].s; + if (tr[i2_3] == mb) { + --tr[i2_3]; + ++dt; + } + } + mbt = mb; + } + return { t: new u8(tr), l: mbt }; +}; +var ln = function(n, l, d) { + return n.s == -1 ? Math.max(ln(n.l, l, d + 1), ln(n.r, l, d + 1)) : l[n.s] = d; +}; +var lc = function(c) { + var s = c.length; + while (s && !c[--s]) + ; + var cl = new u16(++s); + var cli = 0, cln = c[0], cls = 1; + var w = function(v) { + cl[cli++] = v; + }; + for (var i2 = 1;i2 <= s; ++i2) { + if (c[i2] == cln && i2 != s) + ++cls; + else { + if (!cln && cls > 2) { + for (;cls > 138; cls -= 138) + w(32754); + if (cls > 2) { + w(cls > 10 ? cls - 11 << 5 | 28690 : cls - 3 << 5 | 12305); + cls = 0; + } + } else if (cls > 3) { + w(cln), --cls; + for (;cls > 6; cls -= 6) + w(8304); + if (cls > 2) + w(cls - 3 << 5 | 8208), cls = 0; + } + while (cls--) + w(cln); + cls = 1; + cln = c[i2]; + } + } + return { c: cl.subarray(0, cli), n: s }; +}; +var clen = function(cf, cl) { + var l = 0; + for (var i2 = 0;i2 < cl.length; ++i2) + l += cf[i2] * cl[i2]; + return l; +}; +var wfblk = function(out, pos, dat) { + var s = dat.length; + var o = shft(pos + 2); + out[o] = s & 255; + out[o + 1] = s >> 8; + out[o + 2] = out[o] ^ 255; + out[o + 3] = out[o + 1] ^ 255; + for (var i2 = 0;i2 < s; ++i2) + out[o + i2 + 4] = dat[i2]; + return (o + 4 + s) * 8; +}; +var wblk = function(dat, out, final, syms, lf, df, eb, li, bs, bl, p) { + wbits(out, p++, final); + ++lf[256]; + var _a2 = hTree(lf, 15), dlt = _a2.t, mlb = _a2.l; + var _b2 = hTree(df, 15), ddt = _b2.t, mdb = _b2.l; + var _c = lc(dlt), lclt = _c.c, nlc = _c.n; + var _d = lc(ddt), lcdt = _d.c, ndc = _d.n; + var lcfreq = new u16(19); + for (var i2 = 0;i2 < lclt.length; ++i2) + ++lcfreq[lclt[i2] & 31]; + for (var i2 = 0;i2 < lcdt.length; ++i2) + ++lcfreq[lcdt[i2] & 31]; + var _e = hTree(lcfreq, 7), lct = _e.t, mlcb = _e.l; + var nlcc = 19; + for (;nlcc > 4 && !lct[clim[nlcc - 1]]; --nlcc) + ; + var flen = bl + 5 << 3; + var ftlen = clen(lf, flt) + clen(df, fdt) + eb; + var dtlen = clen(lf, dlt) + clen(df, ddt) + eb + 14 + 3 * nlcc + clen(lcfreq, lct) + 2 * lcfreq[16] + 3 * lcfreq[17] + 7 * lcfreq[18]; + if (bs >= 0 && flen <= ftlen && flen <= dtlen) + return wfblk(out, p, dat.subarray(bs, bs + bl)); + var lm, ll, dm, dl; + wbits(out, p, 1 + (dtlen < ftlen)), p += 2; + if (dtlen < ftlen) { + lm = hMap(dlt, mlb, 0), ll = dlt, dm = hMap(ddt, mdb, 0), dl = ddt; + var llm = hMap(lct, mlcb, 0); + wbits(out, p, nlc - 257); + wbits(out, p + 5, ndc - 1); + wbits(out, p + 10, nlcc - 4); + p += 14; + for (var i2 = 0;i2 < nlcc; ++i2) + wbits(out, p + 3 * i2, lct[clim[i2]]); + p += 3 * nlcc; + var lcts = [lclt, lcdt]; + for (var it = 0;it < 2; ++it) { + var clct = lcts[it]; + for (var i2 = 0;i2 < clct.length; ++i2) { + var len = clct[i2] & 31; + wbits(out, p, llm[len]), p += lct[len]; + if (len > 15) + wbits(out, p, clct[i2] >> 5 & 127), p += clct[i2] >> 12; + } + } + } else { + lm = flm, ll = flt, dm = fdm, dl = fdt; + } + for (var i2 = 0;i2 < li; ++i2) { + var sym = syms[i2]; + if (sym > 255) { + var len = sym >> 18 & 31; + wbits16(out, p, lm[len + 257]), p += ll[len + 257]; + if (len > 7) + wbits(out, p, sym >> 23 & 31), p += fleb[len]; + var dst = sym & 31; + wbits16(out, p, dm[dst]), p += dl[dst]; + if (dst > 3) + wbits16(out, p, sym >> 5 & 8191), p += fdeb[dst]; + } else { + wbits16(out, p, lm[sym]), p += ll[sym]; + } + } + wbits16(out, p, lm[256]); + return p + ll[256]; +}; +var deo = /* @__PURE__ */ new i32([65540, 131080, 131088, 131104, 262176, 1048704, 1048832, 2114560, 2117632]); +var et = /* @__PURE__ */ new u8(0); +var dflt = function(dat, lvl, plvl, pre, post, st) { + var s = st.z || dat.length; + var o = new u8(pre + s + 5 * (1 + Math.ceil(s / 7000)) + post); + var w = o.subarray(pre, o.length - post); + var lst = st.l; + var pos = (st.r || 0) & 7; + if (lvl) { + if (pos) + w[0] = st.r >> 3; + var opt = deo[lvl - 1]; + var n = opt >> 13, c = opt & 8191; + var msk_1 = (1 << plvl) - 1; + var prev = st.p || new u16(32768), head = st.h || new u16(msk_1 + 1); + var bs1_1 = Math.ceil(plvl / 3), bs2_1 = 2 * bs1_1; + var hsh = function(i3) { + return (dat[i3] ^ dat[i3 + 1] << bs1_1 ^ dat[i3 + 2] << bs2_1) & msk_1; + }; + var syms = new i32(25000); + var lf = new u16(288), df = new u16(32); + var lc_1 = 0, eb = 0, i2 = st.i || 0, li = 0, wi = st.w || 0, bs = 0; + for (;i2 + 2 < s; ++i2) { + var hv = hsh(i2); + var imod = i2 & 32767, pimod = head[hv]; + prev[imod] = pimod; + head[hv] = imod; + if (wi <= i2) { + var rem = s - i2; + if ((lc_1 > 7000 || li > 24576) && (rem > 423 || !lst)) { + pos = wblk(dat, w, 0, syms, lf, df, eb, li, bs, i2 - bs, pos); + li = lc_1 = eb = 0, bs = i2; + for (var j = 0;j < 286; ++j) + lf[j] = 0; + for (var j = 0;j < 30; ++j) + df[j] = 0; + } + var l = 2, d = 0, ch_1 = c, dif = imod - pimod & 32767; + if (rem > 2 && hv == hsh(i2 - dif)) { + var maxn = Math.min(n, rem) - 1; + var maxd = Math.min(32767, i2); + var ml = Math.min(258, rem); + while (dif <= maxd && --ch_1 && imod != pimod) { + if (dat[i2 + l] == dat[i2 + l - dif]) { + var nl = 0; + for (;nl < ml && dat[i2 + nl] == dat[i2 + nl - dif]; ++nl) + ; + if (nl > l) { + l = nl, d = dif; + if (nl > maxn) + break; + var mmd = Math.min(dif, nl - 2); + var md = 0; + for (var j = 0;j < mmd; ++j) { + var ti = i2 - dif + j & 32767; + var pti = prev[ti]; + var cd = ti - pti & 32767; + if (cd > md) + md = cd, pimod = ti; + } + } + } + imod = pimod, pimod = prev[imod]; + dif += imod - pimod & 32767; + } + } + if (d) { + syms[li++] = 268435456 | revfl[l] << 18 | revfd[d]; + var lin = revfl[l] & 31, din = revfd[d] & 31; + eb += fleb[lin] + fdeb[din]; + ++lf[257 + lin]; + ++df[din]; + wi = i2 + l; + ++lc_1; + } else { + syms[li++] = dat[i2]; + ++lf[dat[i2]]; + } + } + } + for (i2 = Math.max(i2, wi);i2 < s; ++i2) { + syms[li++] = dat[i2]; + ++lf[dat[i2]]; + } + pos = wblk(dat, w, lst, syms, lf, df, eb, li, bs, i2 - bs, pos); + if (!lst) { + st.r = pos & 7 | w[pos / 8 | 0] << 3; + pos -= 7; + st.h = head, st.p = prev, st.i = i2, st.w = wi; + } + } else { + for (var i2 = st.w || 0;i2 < s + lst; i2 += 65535) { + var e = i2 + 65535; + if (e >= s) { + w[pos / 8 | 0] = lst; + e = s; + } + pos = wfblk(w, pos + 1, dat.subarray(i2, e)); + } + st.i = s; + } + return slc(o, 0, pre + shft(pos) + post); +}; +var crct = /* @__PURE__ */ function() { + var t = new Int32Array(256); + for (var i2 = 0;i2 < 256; ++i2) { + var c = i2, k = 9; + while (--k) + c = (c & 1 && -306674912) ^ c >>> 1; + t[i2] = c; + } + return t; +}(); +var crc = function() { + var c = -1; + return { + p: function(d) { + var cr = c; + for (var i2 = 0;i2 < d.length; ++i2) + cr = crct[cr & 255 ^ d[i2]] ^ cr >>> 8; + c = cr; + }, + d: function() { + return ~c; + } + }; +}; +var dopt = function(dat, opt, pre, post, st) { + if (!st) { + st = { l: 1 }; + if (opt.dictionary) { + var dict = opt.dictionary.subarray(-32768); + var newDat = new u8(dict.length + dat.length); + newDat.set(dict); + newDat.set(dat, dict.length); + dat = newDat; + st.w = dict.length; + } + } + return dflt(dat, opt.level == null ? 6 : opt.level, opt.mem == null ? st.l ? Math.ceil(Math.max(8, Math.min(13, Math.log(dat.length))) * 1.5) : 20 : 12 + opt.mem, pre, post, st); +}; +var mrg = function(a, b) { + var o = {}; + for (var k in a) + o[k] = a[k]; + for (var k in b) + o[k] = b[k]; + return o; +}; +var wbytes = function(d, b, v) { + for (;v; ++b) + d[b] = v, v >>>= 8; +}; +var Deflate = /* @__PURE__ */ function() { + function Deflate2(opts, cb) { + if (typeof opts == "function") + cb = opts, opts = {}; + this.ondata = cb; + this.o = opts || {}; + this.s = { l: 0, i: 32768, w: 32768, z: 32768 }; + this.b = new u8(98304); + if (this.o.dictionary) { + var dict = this.o.dictionary.subarray(-32768); + this.b.set(dict, 32768 - dict.length); + this.s.i = 32768 - dict.length; + } + } + Deflate2.prototype.p = function(c, f) { + this.ondata(dopt(c, this.o, 0, 0, this.s), f); + }; + Deflate2.prototype.push = function(chunk, final) { + if (!this.ondata) + err(5); + if (this.s.l) + err(4); + var endLen = chunk.length + this.s.z; + if (endLen > this.b.length) { + if (endLen > 2 * this.b.length - 32768) { + var newBuf = new u8(endLen & -32768); + newBuf.set(this.b.subarray(0, this.s.z)); + this.b = newBuf; + } + var split = this.b.length - this.s.z; + this.b.set(chunk.subarray(0, split), this.s.z); + this.s.z = this.b.length; + this.p(this.b, false); + this.b.set(this.b.subarray(-32768)); + this.b.set(chunk.subarray(split), 32768); + this.s.z = chunk.length - split + 32768; + this.s.i = 32766, this.s.w = 32768; + } else { + this.b.set(chunk, this.s.z); + this.s.z += chunk.length; + } + this.s.l = final & 1; + if (this.s.z > this.s.w + 8191 || final) { + this.p(this.b, final || false); + this.s.w = this.s.i, this.s.i -= 2; + } + if (final) { + this.s = this.o = {}; + this.b = et; + } + }; + Deflate2.prototype.flush = function(sync) { + if (!this.ondata) + err(5); + if (this.s.l) + err(4); + this.p(this.b, false); + this.s.w = this.s.i, this.s.i -= 2; + if (sync) { + var c = new u8(6); + c[0] = this.s.r >> 3; + var ep = wfblk(c, this.s.r, et); + this.s.r = 0; + this.ondata(c.subarray(0, ep >> 3), false); + } + }; + return Deflate2; +}(); +var te = typeof TextEncoder != "undefined" && /* @__PURE__ */ new TextEncoder; +var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder; +var tds = 0; +try { + td.decode(et, { stream: true }); + tds = 1; +} catch (e) {} +function strToU8(str, latin1) { + if (latin1) { + var ar_1 = new u8(str.length); + for (var i2 = 0;i2 < str.length; ++i2) + ar_1[i2] = str.charCodeAt(i2); + return ar_1; + } + if (te) + return te.encode(str); + var l = str.length; + var ar = new u8(str.length + (str.length >> 1)); + var ai = 0; + var w = function(v) { + ar[ai++] = v; + }; + for (var i2 = 0;i2 < l; ++i2) { + if (ai + 5 > ar.length) { + var n = new u8(ai + 8 + (l - i2 << 1)); + n.set(ar); + ar = n; + } + var c = str.charCodeAt(i2); + if (c < 128 || latin1) + w(c); + else if (c < 2048) + w(192 | c >> 6), w(128 | c & 63); + else if (c > 55295 && c < 57344) + c = 65536 + (c & 1023 << 10) | str.charCodeAt(++i2) & 1023, w(240 | c >> 18), w(128 | c >> 12 & 63), w(128 | c >> 6 & 63), w(128 | c & 63); + else + w(224 | c >> 12), w(128 | c >> 6 & 63), w(128 | c & 63); + } + return slc(ar, 0, ai); +} +var dbf = function(l) { + return l == 1 ? 3 : l < 6 ? 2 : l == 9 ? 1 : 0; +}; +var exfl = function(ex) { + var le = 0; + if (ex) { + for (var k in ex) { + var l = ex[k].length; + if (l > 65535) + err(9); + le += l + 4; + } + } + return le; +}; +var wzh = function(d, b, f, fn, u, c, ce, co) { + var fl2 = fn.length, ex = f.extra, col = co && co.length; + var exl = exfl(ex); + wbytes(d, b, ce != null ? 33639248 : 67324752), b += 4; + if (ce != null) + d[b++] = 20, d[b++] = f.os; + d[b] = 20, b += 2; + d[b++] = f.flag << 1 | (c < 0 && 8), d[b++] = u && 8; + d[b++] = f.compression & 255, d[b++] = f.compression >> 8; + var dt = new Date(f.mtime == null ? Date.now() : f.mtime), y = dt.getFullYear() - 1980; + if (y < 0 || y > 119) + err(10); + wbytes(d, b, y << 25 | dt.getMonth() + 1 << 21 | dt.getDate() << 16 | dt.getHours() << 11 | dt.getMinutes() << 5 | dt.getSeconds() >> 1), b += 4; + if (c != -1) { + wbytes(d, b, f.crc); + wbytes(d, b + 4, c < 0 ? -c - 2 : c); + wbytes(d, b + 8, f.size); + } + wbytes(d, b + 12, fl2); + wbytes(d, b + 14, exl), b += 16; + if (ce != null) { + wbytes(d, b, col); + wbytes(d, b + 6, f.attrs); + wbytes(d, b + 10, ce), b += 14; + } + d.set(fn, b); + b += fl2; + if (exl) { + for (var k in ex) { + var exf = ex[k], l = exf.length; + wbytes(d, b, +k); + wbytes(d, b + 2, l); + d.set(exf, b + 4), b += 4 + l; + } + } + if (col) + d.set(co, b), b += col; + return b; +}; +var wzf = function(o, b, c, d, e) { + wbytes(o, b, 101010256); + wbytes(o, b + 8, c); + wbytes(o, b + 10, c); + wbytes(o, b + 12, d); + wbytes(o, b + 16, e); +}; +var ZipPassThrough = /* @__PURE__ */ function() { + function ZipPassThrough2(filename) { + this.filename = filename; + this.c = crc(); + this.size = 0; + this.compression = 0; + } + ZipPassThrough2.prototype.process = function(chunk, final) { + this.ondata(null, chunk, final); + }; + ZipPassThrough2.prototype.push = function(chunk, final) { + if (!this.ondata) + err(5); + this.c.p(chunk); + this.size += chunk.length; + if (final) + this.crc = this.c.d(); + this.process(chunk, final || false); + }; + return ZipPassThrough2; +}(); +var ZipDeflate = /* @__PURE__ */ function() { + function ZipDeflate2(filename, opts) { + var _this = this; + if (!opts) + opts = {}; + ZipPassThrough.call(this, filename); + this.d = new Deflate(opts, function(dat, final) { + _this.ondata(null, dat, final); + }); + this.compression = 8; + this.flag = dbf(opts.level); + } + ZipDeflate2.prototype.process = function(chunk, final) { + try { + this.d.push(chunk, final); + } catch (e) { + this.ondata(e, null, final); + } + }; + ZipDeflate2.prototype.push = function(chunk, final) { + ZipPassThrough.prototype.push.call(this, chunk, final); + }; + return ZipDeflate2; +}(); +var Zip = /* @__PURE__ */ function() { + function Zip2(cb) { + this.ondata = cb; + this.u = []; + this.d = 1; + } + Zip2.prototype.add = function(file) { + var _this = this; + if (!this.ondata) + err(5); + if (this.d & 2) + this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, false); + else { + var f = strToU8(file.filename), fl_1 = f.length; + var com = file.comment, o = com && strToU8(com); + var u = fl_1 != file.filename.length || o && com.length != o.length; + var hl_1 = fl_1 + exfl(file.extra) + 30; + if (fl_1 > 65535) + this.ondata(err(11, 0, 1), null, false); + var header = new u8(hl_1); + wzh(header, 0, file, f, u, -1); + var chks_1 = [header]; + var pAll_1 = function() { + for (var _i = 0, chks_2 = chks_1;_i < chks_2.length; _i++) { + var chk = chks_2[_i]; + _this.ondata(null, chk, false); + } + chks_1 = []; + }; + var tr_1 = this.d; + this.d = 0; + var ind_1 = this.u.length; + var uf_1 = mrg(file, { + f, + u, + o, + t: function() { + if (file.terminate) + file.terminate(); + }, + r: function() { + pAll_1(); + if (tr_1) { + var nxt = _this.u[ind_1 + 1]; + if (nxt) + nxt.r(); + else + _this.d = 1; + } + tr_1 = 1; + } + }); + var cl_1 = 0; + file.ondata = function(err2, dat, final) { + if (err2) { + _this.ondata(err2, dat, final); + _this.terminate(); + } else { + cl_1 += dat.length; + chks_1.push(dat); + if (final) { + var dd = new u8(16); + wbytes(dd, 0, 134695760); + wbytes(dd, 4, file.crc); + wbytes(dd, 8, cl_1); + wbytes(dd, 12, file.size); + chks_1.push(dd); + uf_1.c = cl_1, uf_1.b = hl_1 + cl_1 + 16, uf_1.crc = file.crc, uf_1.size = file.size; + if (tr_1) + uf_1.r(); + tr_1 = 1; + } else if (tr_1) + pAll_1(); + } + }; + this.u.push(uf_1); + } + }; + Zip2.prototype.end = function() { + var _this = this; + if (this.d & 2) { + this.ondata(err(4 + (this.d & 1) * 8, 0, 1), null, true); + return; + } + if (this.d) + this.e(); + else + this.u.push({ + r: function() { + if (!(_this.d & 1)) + return; + _this.u.splice(-1, 1); + _this.e(); + }, + t: function() {} + }); + this.d = 3; + }; + Zip2.prototype.e = function() { + var bt = 0, l = 0, tl = 0; + for (var _i = 0, _a2 = this.u;_i < _a2.length; _i++) { + var f = _a2[_i]; + tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0); + } + var out = new u8(tl + 22); + for (var _b2 = 0, _c = this.u;_b2 < _c.length; _b2++) { + var f = _c[_b2]; + wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o); + bt += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b; + } + wzf(out, bt, this.u.length, tl, l); + this.ondata(null, out, true); + this.d = 2; + }; + Zip2.prototype.terminate = function() { + for (var _i = 0, _a2 = this.u;_i < _a2.length; _i++) { + var f = _a2[_i]; + f.t(); + } + this.d = 2; + }; + return Zip2; +}(); + +// src/session-export-shared.ts +var NETXOPS_SESSIONS_EXPORT_PATH = "/api/netxops.sessions.export"; + +// src/netx/session-export.ts +var COMPRESSION_LEVEL = 6; +var PUSH_CHUNK_CODE_UNITS = 1 << 16; +function resolveSessionPersistence(ctx) { + const fromGet = typeof ctx.get === "function" ? ctx.get("sessionPersistence") : undefined; + const persistence = fromGet ?? ctx.sessionPersistence; + if (!persistence || typeof persistence.list !== "function" || typeof persistence.readRaw !== "function") { + return; + } + return persistence; +} +function resolveSessionStore(ctx) { + const fromGet = typeof ctx.get === "function" ? ctx.get("sessions") : undefined; + const sessions = fromGet ?? ctx.sessions; + if (!sessions || typeof sessions.get !== "function" || typeof sessions.flush !== "function") { + return; + } + return sessions; +} +async function getSessionsExportStatus(ctx, signal) { + const persistence = resolveSessionPersistence(ctx); + if (!persistence) { + return { + available: false, + sessionCount: 0, + supportsRawArtifacts: false, + reason: "sessionPersistence unavailable — mount a JSONL session backend (web profile default)" + }; + } + if (!persistence.supportsRawArtifacts) { + return { + available: false, + sessionCount: 0, + supportsRawArtifacts: false, + reason: "persistence backend does not expose per-session raw artifacts (SQLite export unsupported)" + }; + } + try { + const headers = await persistence.list(signal); + signal?.throwIfAborted(); + return { + available: true, + sessionCount: headers.length, + supportsRawArtifacts: true + }; + } catch (error) { + return { + available: false, + sessionCount: 0, + supportsRawArtifacts: true, + reason: error instanceof Error ? error.message : String(error) + }; + } +} +function safePathSegment(id) { + return id.replace(/[^A-Za-z0-9_-]/g, "_"); +} +function sessionsExportZipFilename(exportedAt = new Date, host = osHostname()) { + const stamp = [ + exportedAt.getUTCFullYear(), + String(exportedAt.getUTCMonth() + 1).padStart(2, "0"), + String(exportedAt.getUTCDate()).padStart(2, "0"), + "-", + String(exportedAt.getUTCHours()).padStart(2, "0"), + String(exportedAt.getUTCMinutes()).padStart(2, "0"), + String(exportedAt.getUTCSeconds()).padStart(2, "0") + ].join(""); + return `dsh-sessions-${safePathSegment(host)}-${stamp}.zip`; +} +async function flushLiveSession(sessions, id, signal) { + signal?.throwIfAborted(); + if (!sessions?.get || !sessions.flush) + return; + const live = sessions.get(id); + if (live === undefined || live === null) + return; + await sessions.flush(live); + signal?.throwIfAborted(); +} +async function* sessionsExportEntries(ctx, signal) { + const persistence = resolveSessionPersistence(ctx); + if (!persistence) { + throw new Error("sessionPersistence unavailable"); + } + if (!persistence.supportsRawArtifacts) { + throw new Error("persistence backend does not expose per-session raw artifacts"); + } + const sessions = resolveSessionStore(ctx); + const headers = await persistence.list(signal); + signal?.throwIfAborted(); + const exportedAt = new Date().toISOString(); + const host = osHostname(); + const included = []; + const skipped = []; + const artifactEntries = []; + for (const header of headers) { + signal?.throwIfAborted(); + const id = String(header.id); + try { + await flushLiveSession(sessions, id, signal); + const raw = await persistence.readRaw(id, signal); + signal?.throwIfAborted(); + if (raw === undefined) { + skipped.push({ id, reason: "no stored artifact" }); + continue; + } + const filename = raw.filename && raw.filename.length > 0 ? raw.filename : "session.jsonl"; + const path = `sessions/${safePathSegment(id)}/${filename}`; + artifactEntries.push({ path, content: raw.content }); + included.push({ + id, + path, + createdAt: header.createdAt, + cwd: header.cwd, + agentPreset: header.agentPreset, + parentSession: header.parentSession !== undefined ? String(header.parentSession) : undefined, + origin: header.origin + }); + } catch (error) { + skipped.push({ + id, + reason: error instanceof Error ? error.message : String(error) + }); + } + } + const manifest = { + kind: "dsh-netxops-sessions-export", + version: 1, + exportedAt, + hostname: host, + sessionCountListed: headers.length, + sessionCountIncluded: included.length, + sessionCountSkipped: skipped.length, + sessions: included, + skipped + }; + yield { + path: "manifest.json", + content: `${JSON.stringify(manifest, null, 2)} +` + }; + for (const entry of artifactEntries) { + signal?.throwIfAborted(); + yield entry; + } +} +async function pushArtifactChunks(deflate, content, signal) { + const encoder = new TextEncoder; + let offset = 0; + let finalChunk; + do { + signal.throwIfAborted(); + let end = Math.min(offset + PUSH_CHUNK_CODE_UNITS, content.length); + if (end < content.length && end - offset > 1) { + const last = content.charCodeAt(end - 1); + if (last >= 55296 && last <= 56319) + end -= 1; + } + finalChunk = end >= content.length; + deflate.push(encoder.encode(content.slice(offset, end)), finalChunk); + offset = end; + } while (!finalChunk); +} +function streamSessionsExportZip(ctx, signal) { + const consumerAbort = new AbortController; + const producerSignal = AbortSignal.any([signal, consumerAbort.signal]); + let zip; + let zipTerminated = false; + const terminateZip = () => { + if (zip === undefined || zipTerminated) + return; + zipTerminated = true; + zip.terminate(); + }; + return new ReadableStream({ + start(controller) { + const archive = new Zip((error, data, final) => { + if (error) { + controller.error(error); + return; + } + if (data.byteLength > 0) + controller.enqueue(data); + if (final) + controller.close(); + }); + zip = archive; + (async () => { + try { + for await (const entry of sessionsExportEntries(ctx, producerSignal)) { + const deflate = new ZipDeflate(entry.path, { level: COMPRESSION_LEVEL }); + archive.add(deflate); + await pushArtifactChunks(deflate, entry.content, producerSignal); + } + archive.end(); + } catch (error) { + terminateZip(); + controller.error(error instanceof Error ? error : new Error(String(error))); + } + })(); + }, + cancel(reason) { + consumerAbort.abort(reason instanceof Error ? reason : new Error("sessions export stream cancelled")); + terminateZip(); + } + }); +} +async function sessionsExportResponse(ctx, request) { + const status = await getSessionsExportStatus(ctx, request.signal); + if (!status.available) { + return new Response(status.reason ?? "sessions export unavailable", { + status: status.supportsRawArtifacts === false && status.reason?.includes("raw artifacts") ? 501 : 500 + }); + } + const filename = sessionsExportZipFilename(); + const body = streamSessionsExportZip(ctx, request.signal); + return new Response(body, { + headers: { + "content-type": "application/zip", + "content-disposition": `attachment; filename="${filename}"`, + "x-netxops-session-count": String(status.sessionCount) + } + }); +} +async function sessionsExportHeadResponse(ctx, request) { + const status = await getSessionsExportStatus(ctx, request.signal); + if (!status.available) { + return new Response(null, { + status: status.supportsRawArtifacts === false && status.reason?.includes("raw artifacts") ? 501 : 500, + headers: { + "x-netxops-export-error": status.reason ?? "sessions export unavailable" + } + }); + } + const filename = sessionsExportZipFilename(); + return new Response(null, { + status: 200, + headers: { + "content-type": "application/zip", + "content-disposition": `attachment; filename="${filename}"`, + "x-netxops-session-count": String(status.sessionCount) + } + }); +} + // src/netx/tools.ts import { defineTool } from "@deepseek-ai/dsh-tools"; @@ -851,7 +2000,7 @@ function strList(args, key) { const v = args[key]; if (!Array.isArray(v)) return []; - return v.map((x) => String(x).trim()).filter((x) => x.length > 0); + return v.map((x2) => String(x2).trim()).filter((x2) => x2.length > 0); } function clampInt(value, fallback, min, max) { const n = value === undefined ? fallback : Math.trunc(value); @@ -1161,7 +2310,7 @@ function strList2(args, key) { const v = args[key]; if (!Array.isArray(v)) return []; - return v.map((x) => String(x).trim()).filter((x) => x.length > 0); + return v.map((x2) => String(x2).trim()).filter((x2) => x2.length > 0); } function clampInt2(value, fallback, min, max) { const n = value === undefined ? fallback : Math.trunc(value); @@ -1650,15 +2799,15 @@ async function analyzeTopologyViewLayout(client, args, signal) { let missingxy = 0; for (const raw of nodes) { const row = asRecord2(raw); - const x = Number(row.x); + const x2 = Number(row.x); const y = Number(row.y); - if (!Number.isFinite(x) || !Number.isFinite(y)) { + if (!Number.isFinite(x2) || !Number.isFinite(y)) { missingxy += 1; continue; } - minX = Math.min(minX, x); + minX = Math.min(minX, x2); minY = Math.min(minY, y); - maxX = Math.max(maxX, x); + maxX = Math.max(maxX, x2); maxY = Math.max(maxY, y); } const degree = new Map; @@ -2151,6 +3300,7 @@ var Config = z.object({ alarmPushEnabled: z.boolean().default(false), alarmDeliverDsh: z.boolean().default(true), alarmDeliverIm: z.boolean().default(false), + imTargets: z.string().default(""), imBotId: z.string().default(""), imTargetId: z.string().default(""), nmsProvider: z.string().default("zte-ume"), @@ -2166,7 +3316,7 @@ function resolveDshHome() { const fromEnv = process.env.DSH_HOME?.trim(); if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv; - return join3(homedir2(), ".dsh"); + return join3(homedir3(), ".dsh"); } function ensureAgentPresetInstalled(logger) { const src = join3(packageRoot(), "presets", NETXOPS_PRESET_ID); @@ -2233,9 +3383,12 @@ function apply(ctx, config = Config({})) { } const lang = current.lang; const deliverDsh = current.alarmDeliverDsh !== false; - const deliverIm = current.alarmDeliverIm === true; - const imBotId = current.imBotId ?? ""; - const imTargetId = current.imTargetId ?? ""; + const imTargets = resolveImTargets({ + imTargets: current.imTargets ?? "", + imBotId: current.imBotId ?? "", + imTargetId: current.imTargetId ?? "" + }); + const deliverIm = imTargets.length > 0; stopAlarmPush = startAlarmPushClient({ apiUrl, token, @@ -2246,8 +3399,7 @@ function apply(ctx, config = Config({})) { } await deliverAlarmToIm(ctx, payload, { enabled: deliverIm, - botId: imBotId, - targetId: imTargetId, + targets: imTargets, lang }); } @@ -2277,7 +3429,12 @@ function apply(ctx, config = Config({})) { 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 dsh=%s im=%s public=[%s]", apiUrl, current.alarmPushEnabled === true, current.alarmDeliverDsh !== false, current.alarmDeliverIm === true, groupsForPlane(groups, "public").join(",") || "(none)"); + const imSinkCount = resolveImTargets({ + imTargets: current.imTargets ?? "", + imBotId: current.imBotId ?? "", + imTargetId: current.imTargetId ?? "" + }).length; + ctx.logger.info("netxops: published connection → %s tokenConfigured=true alarmPush=%s dsh=%s im=%s public=[%s]", apiUrl, current.alarmPushEnabled === true, current.alarmDeliverDsh !== false, imSinkCount, groupsForPlane(groups, "public").join(",") || "(none)"); } }).catch((error) => { ctx.logger.error("netxops: connection publish error: %s", error); @@ -2350,55 +3507,80 @@ function apply(ctx, config = Config({})) { }, "netxops: dispose public skills"); }); ctx.inject(["connection"], (connCtx) => { - const rpc = connCtx.connection?.rpc; + const connection = connCtx.connection; + const rpc = connection?.rpc; if (!rpc || typeof rpc.handle !== "function") { connCtx.logger.warn("netxops: connection.rpc.handle unavailable — alarm status UI disabled"); - return; + } else { + connCtx.effect(() => { + const dispose = rpc.handle(NETXOPS_RPC_CHANNEL, async (endpoint) => { + if (endpoint === "alarm-push.status") { + return { ok: true, value: getAlarmPushStatus() }; + } + if (endpoint === "sessions.export.status") { + const value = await getSessionsExportStatus(ctx); + return { ok: true, value }; + } + if (endpoint === "im-delivery.catalog") { + const fromGet = typeof ctx.get === "function" ? ctx.get("dshIm") : undefined; + const im = fromGet ?? ctx.dshIm; + if (!im || typeof im.listDeliveryCatalog !== "function") { + return { + ok: true, + value: { + available: false, + options: [], + hint: "dsh-im-ops missing or outdated — install ≥ops.24 for delivery picker" + } + }; + } + try { + const options = await im.listDeliveryCatalog(); + return { + ok: true, + value: { + available: true, + options: Array.isArray(options) ? options : [] + } + }; + } catch (error) { + return { + ok: true, + value: { + available: false, + options: [], + hint: error instanceof Error ? error.message : String(error) + } + }; + } + } + return { ok: false, error: { code: "bad-request", message: "Unknown endpoint." } }; + }); + return () => { + dispose(); + }; + }, "netxops: alarm-push status rpc"); } - connCtx.effect(() => { - const dispose = rpc.handle(NETXOPS_RPC_CHANNEL, async (endpoint) => { - if (endpoint === "alarm-push.status") { - return { ok: true, value: getAlarmPushStatus() }; - } - if (endpoint === "im-delivery.catalog") { - const fromGet = typeof ctx.get === "function" ? ctx.get("dshIm") : undefined; - const im = fromGet ?? ctx.dshIm; - if (!im || typeof im.listDeliveryCatalog !== "function") { - return { - ok: true, - value: { - available: false, - options: [], - hint: "dsh-im-ops missing or outdated — install ≥ops.24 for delivery picker" - } - }; + const fetchApi = connection?.fetch; + if (!fetchApi || typeof fetchApi.register !== "function") { + connCtx.logger.warn("netxops: connection.fetch.register unavailable — sessions export download disabled"); + } else { + connCtx.effect(() => { + const dispose = fetchApi.register({ + path: NETXOPS_SESSIONS_EXPORT_PATH, + methods: ["GET", "HEAD"], + fetch: async (request) => { + if (request.method === "HEAD") { + return sessionsExportHeadResponse(ctx, request); + } + return sessionsExportResponse(ctx, request); } - try { - const options = await im.listDeliveryCatalog(); - return { - ok: true, - value: { - available: true, - options: Array.isArray(options) ? options : [] - } - }; - } catch (error) { - return { - ok: true, - value: { - available: false, - options: [], - hint: error instanceof Error ? error.message : String(error) - } - }; - } - } - return { ok: false, error: { code: "bad-request", message: "Unknown endpoint." } }; - }); - return () => { - dispose(); - }; - }, "netxops: alarm-push status rpc"); + }); + return () => { + dispose(); + }; + }, "netxops: sessions export fetch"); + } }); ctx.effect(() => () => { generation += 1; diff --git a/package.json b/package.json index d43d142..5ff1bbc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "dsh-netxops", - "version": "0.1.24", - "description": "DeepSeek Harness Netx Ops: capability groups ops/topology (one group ↔ one skill)", + "version": "0.1.26", + "description": "DeepSeek Harness Netx Ops: capability groups ops/topology + multi IM delivery + session export", "license": "MIT", "type": "module", "private": false, @@ -89,7 +89,8 @@ "react": "^18.2.0 || ^19.0.0" }, "dependencies": { - "@deepseek-ai/schemastery": "*" + "@deepseek-ai/schemastery": "*", + "fflate": "^0.8.2" }, "devDependencies": { "@types/react": "~18.3.1", diff --git a/src/client/NetxopsCard.tsx b/src/client/NetxopsCard.tsx index 842d2aa..549f070 100644 --- a/src/client/NetxopsCard.tsx +++ b/src/client/NetxopsCard.tsx @@ -9,9 +9,15 @@ import type { NetxopsLocaleKey } from './locales.ts' import type { CardFieldState } from './card-form.ts' import { imCatalogOptionKey, - parseImCatalogOptionKey, type ImDeliveryCatalog, } from './im-delivery-catalog.ts' +import { + formatImTargetsJson, + imTargetKey, + resolveImTargets, + setImTargetSelected, + type ImDeliveryTarget, +} from '../netx/im-targets.ts' import { alarmPushTone, type AlarmPushPhase } from './alarm-push-status-view.ts' import { ensureStyles } from './styles.ts' import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client' @@ -95,117 +101,98 @@ function ValueField(props: { function ImDeliveryPicker(props: { catalog: ImDeliveryCatalog - botId: string - targetId: string + targetsJson: string + legacyBotId: string + legacyTargetId: string disabled: boolean labels: { target: string none: string - manual: string + selectedCount: string unavailable: string - botId: string - botHint: string - targetId: string - targetHint: string - overridden: string - reset: string - invalid: string } - botField: CardFieldState - targetField: CardFieldState - onPick: (botId: string, targetId: string) => void - onEditBot: (text: string) => void - onEditTarget: (text: string) => void - onResetBot: () => void - onResetTarget: () => void + onChange: (targets: ImDeliveryTarget[]) => void }) { const options = props.catalog.options - const selectedKey = props.botId && props.targetId - ? imCatalogOptionKey(props.botId, props.targetId) - : '' - const matched = options.some((row) => imCatalogOptionKey(row.botId, row.targetId) === selectedKey) - const [manual, setManual] = useState(() => !!(selectedKey && !matched)) - useEffect(() => { - if (selectedKey && matched) setManual(false) - else if (selectedKey && !matched) setManual(true) - }, [selectedKey, matched]) - const selectValue = manual ? '__manual__' : (matched ? selectedKey : '') - const showManual = manual || (selectedKey && !matched) + const selected = resolveImTargets({ + imTargets: props.targetsJson, + imBotId: props.legacyBotId, + imTargetId: props.legacyTargetId, + }) + const selectedKeys = new Set(selected.map((row) => imTargetKey(row.botId, row.targetId))) + + // Orphan legacy / JSON targets not present in the live catalog still show as checked rows. + const orphanSelected = selected.filter( + (row) => !options.some( + (opt) => imCatalogOptionKey(opt.botId, opt.targetId) === imTargetKey(row.botId, row.targetId), + ), + ) return ( - <> -
-
- -
- - {!props.catalog.available || options.length === 0 - ?

{props.catalog.hint || props.labels.unavailable}

+
+
+ {props.labels.target} + {selected.length > 0 + ? ( + + {props.labels.selectedCount.replace('{count}', String(selected.length))} + + ) : null}
- {showManual + {options.length === 0 && orphanSelected.length === 0 ? ( - <> - - - +

+ {!props.catalog.available + ? (props.catalog.hint || props.labels.unavailable) + : props.labels.none} +

) + : ( +
+ {options.map((row) => { + const key = imCatalogOptionKey(row.botId, row.targetId) + const checked = selectedKeys.has(key) + return ( + + ) + })} + {orphanSelected.map((row) => { + const key = imTargetKey(row.botId, row.targetId) + return ( + + ) + })} +
+ )} + {!props.catalog.available && options.length > 0 + ?

{props.catalog.hint || props.labels.unavailable}

: null} - +
) } @@ -362,7 +349,6 @@ export function NetxopsCard(props: NetxopsCardProps) {
{t('capabilityGroups')}
-

{t('capabilityGroupsHint')}

{ props.edit('alarmPushEnabled', event.target.checked ? 'true' : 'false') }} /> - {t('alarmPushEnabledHint')} {pushStatus?.wsUrl ?

{pushStatus.wsUrl}

@@ -460,69 +446,77 @@ export function NetxopsCard(props: NetxopsCardProps) { type="checkbox" checked={state.alarmDeliverDsh.text === 'true'} disabled={disabled} + aria-label={t('alarmDeliverDsh')} onChange={(event) => { props.edit('alarmDeliverDsh', event.target.checked ? 'true' : 'false') }} /> - {t('alarmDeliverDshHint')} - -
-
-
- - {state.alarmDeliverIm.overridden - ? ( - - {t('overridden')} - - - ) - : null} -
-
{ - props.edit('imBotId', botId) - props.edit('imTargetId', targetId) + onChange={(targets) => { + props.edit('imTargets', formatImTargetsJson(targets)) + const first = targets[0] + props.edit('imBotId', first?.botId ?? '') + props.edit('imTargetId', first?.targetId ?? '') + props.edit('alarmDeliverIm', targets.length > 0 ? 'true' : 'false') }} - onEditBot={(text) => { props.edit('imBotId', text) }} - onEditTarget={(text) => { props.edit('imTargetId', text) }} - onResetBot={() => { props.resetField('imBotId') }} - onResetTarget={() => { props.resetField('imTargetId') }} /> +
+
+ {t('sessionsExport')} +
+ {state.sessionsExportStatus === null + ?

{t('sessionsExportUnavailable')}

+ : state.sessionsExportStatus.available + ? ( +

+ {t('sessionsExportCount').replace( + '{count}', + String(state.sessionsExportStatus.sessionCount), + )} +

+ ) + : ( +

+ {state.sessionsExportStatus.reason || t('sessionsExportUnavailable')} +

+ )} + {state.sessionsExportError + ?

{state.sessionsExportError}

+ : null} + {state.sessionsExportLastFile && !state.sessionsExportBusy && !state.sessionsExportError + ? ( +

+ {t('sessionsExportDone').replace('{file}', state.sessionsExportLastFile)} +

+ ) + : null} +
+ +
+
{state.failed ?

{t('saveFailed')}

: null}