From 6c9f323368b6ce8d4054422058d5ad1e3529cd38 Mon Sep 17 00:00:00 2001 From: oliver Date: Sat, 10 Oct 2026 16:34:07 +0800 Subject: [PATCH] Heal sparse Desktop overrides and default-publish ops tools (0.1.51). Plugin Manager often leaves a bare netxops enable stub that wipes Config on DSH 0.2; expand missing keys on Host apply, default groupOpsPublic on, and persist false explicitly so other installs get tools without hand-editing patches. Co-authored-by: Cursor --- cordis.bundle.patch.yml | 4 +- cordis.patch.yml | 4 +- docs/INSTALL.md | 2 +- lib/agent-tools-biz-monitor.js | 2 +- lib/agent-tools-common.js | 2 +- lib/agent-tools-nms.js | 2 +- lib/agent-tools-ops.js | 2 +- lib/agent-tools-topology.js | 2 +- lib/agent-tools.js | 2 +- lib/client.js | 4 +- lib/index.js | 494 +++++++++++++++++++++++---------- package.json | 2 +- src/agent-preset-install.ts | 9 + src/client/controller.ts | 3 +- src/client/index.ts | 2 +- src/index.ts | 4 +- src/netx/capability-groups.ts | 17 +- src/netx/heal-profile-entry.ts | 203 ++++++++++++++ 18 files changed, 593 insertions(+), 167 deletions(-) create mode 100644 src/netx/heal-profile-entry.ts diff --git a/cordis.bundle.patch.yml b/cordis.bundle.patch.yml index 75621a3..3594cea 100644 --- a/cordis.bundle.patch.yml +++ b/cordis.bundle.patch.yml @@ -4,7 +4,7 @@ # Host-plane Netx Ops bridge: settings + credentials → connection publish. # Ops-scoped `netx__*` tools mount from the agent preset (`dsh-netxops/tools`). -# Capability groups: ops in preset; topology / public off. +# Capability groups: ops in preset + public on; topology off. # Rule: one group ↔ one skill (netx-ops / netx-topology). # # IM + cron are separate profile bundles (pnpm blockExoticSubdeps forbids @@ -34,7 +34,7 @@ imTargetId: '' imTargets: '' groupOpsInPreset: true - groupOpsPublic: false + groupOpsPublic: true groupTopologyInPreset: false groupTopologyPublic: false groupBizMonitorInPreset: true diff --git a/cordis.patch.yml b/cordis.patch.yml index a2aa5cd..ba43f7b 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -1,6 +1,6 @@ # Host-plane Netx Ops bridge: settings + credentials → connection publish. # Ops-scoped `netx__*` tools mount from the agent preset (`dsh-netxops/tools`). -# Capability groups: ops in preset; topology / public off. +# Capability groups: ops in preset + public on; topology off. # Rule: one group ↔ one skill (netx-ops / netx-topology). # # IM + cron are separate profile bundles (pnpm blockExoticSubdeps forbids @@ -30,7 +30,7 @@ imTargetId: '' imTargets: '' groupOpsInPreset: true - groupOpsPublic: false + groupOpsPublic: true groupTopologyInPreset: false groupTopologyPublic: false groupBizMonitorInPreset: true diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 4867d02..bf4f3c5 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -67,7 +67,7 @@ One command, three **direct** profile bundles. Do **not** nest IM/cron under net 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). + Capability groups: **ops** defaults on (in Netx Ops + published to other presets). Enable **topology** if needed; uncheck **对其他预设公开** to keep tools Netx-Ops-only. Capability toggles auto-save (new sessions after restart if Host just healed a sparse Desktop override). Optional: **知识库** — set package root (MANIFEST v1.0); default mounts `_skills/kb-*` into Netx Ops; optional public. See [KNOWLEDGE_BASE.md](KNOWLEDGE_BASE.md). 2. Restart after install/upgrade so the bundle patch layers apply. Open Settings → **Agent presets** → **Netx Ops** should appear. The Netx Ops preset is **standard + netxops tools/persona** (shell, search, plan, todo, web, workflows, … plus `netx__*`). Re-activate (or restart the Host) so `$DSH_HOME/.agent-presets/netxops` is recomposed from the current Harness `standard` preset. diff --git a/lib/agent-tools-biz-monitor.js b/lib/agent-tools-biz-monitor.js index 8c45b98..11b938b 100644 --- a/lib/agent-tools-biz-monitor.js +++ b/lib/agent-tools-biz-monitor.js @@ -1,6 +1,6 @@ // src/netx/capability-groups.ts var DEFAULT_CAPABILITY_GROUPS = Object.freeze({ - ops: Object.freeze({ inPreset: true, public: false }), + ops: Object.freeze({ inPreset: true, public: true }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }) }); diff --git a/lib/agent-tools-common.js b/lib/agent-tools-common.js index c4b9bdf..ecf0c45 100644 --- a/lib/agent-tools-common.js +++ b/lib/agent-tools-common.js @@ -1,6 +1,6 @@ // src/netx/capability-groups.ts var DEFAULT_CAPABILITY_GROUPS = Object.freeze({ - ops: Object.freeze({ inPreset: true, public: false }), + ops: Object.freeze({ inPreset: true, public: true }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }) }); diff --git a/lib/agent-tools-nms.js b/lib/agent-tools-nms.js index 61c606c..9931e4b 100644 --- a/lib/agent-tools-nms.js +++ b/lib/agent-tools-nms.js @@ -1,6 +1,6 @@ // src/netx/capability-groups.ts var DEFAULT_CAPABILITY_GROUPS = Object.freeze({ - ops: Object.freeze({ inPreset: true, public: false }), + ops: Object.freeze({ inPreset: true, public: true }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }) }); diff --git a/lib/agent-tools-ops.js b/lib/agent-tools-ops.js index 1b3e4f3..547dd8f 100644 --- a/lib/agent-tools-ops.js +++ b/lib/agent-tools-ops.js @@ -1,6 +1,6 @@ // src/netx/capability-groups.ts var DEFAULT_CAPABILITY_GROUPS = Object.freeze({ - ops: Object.freeze({ inPreset: true, public: false }), + ops: Object.freeze({ inPreset: true, public: true }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }) }); diff --git a/lib/agent-tools-topology.js b/lib/agent-tools-topology.js index dcabd2c..e54247b 100644 --- a/lib/agent-tools-topology.js +++ b/lib/agent-tools-topology.js @@ -1,6 +1,6 @@ // src/netx/capability-groups.ts var DEFAULT_CAPABILITY_GROUPS = Object.freeze({ - ops: Object.freeze({ inPreset: true, public: false }), + ops: Object.freeze({ inPreset: true, public: true }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }) }); diff --git a/lib/agent-tools.js b/lib/agent-tools.js index 2f25f69..90f6480 100644 --- a/lib/agent-tools.js +++ b/lib/agent-tools.js @@ -1,6 +1,6 @@ // src/netx/capability-groups.ts var DEFAULT_CAPABILITY_GROUPS = Object.freeze({ - ops: Object.freeze({ inPreset: true, public: false }), + ops: Object.freeze({ inPreset: true, public: true }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }) }); diff --git a/lib/client.js b/lib/client.js index bedcf41..94626b5 100644 --- a/lib/client.js +++ b/lib/client.js @@ -2006,7 +2006,7 @@ class NetxopsCardController { textField("nmsProvider"), textField("kbRoot"), booleanFieldPersistFalse("groupOpsInPreset"), - booleanField("groupOpsPublic"), + booleanFieldPersistFalse("groupOpsPublic"), booleanField("groupTopologyInPreset"), booleanField("groupTopologyPublic"), booleanFieldPersistFalse("groupBizMonitorInPreset"), @@ -2753,7 +2753,7 @@ var MEMORY_DEFAULTS = { imTargetId: "", imTargets: "", groupOpsInPreset: true, - groupOpsPublic: false, + groupOpsPublic: true, groupTopologyInPreset: false, groupTopologyPublic: false, groupBizMonitorInPreset: false, diff --git a/lib/index.js b/lib/index.js index b612922..386f4ac 100644 --- a/lib/index.js +++ b/lib/index.js @@ -9,63 +9,253 @@ import * as DshSettings from "@deepseek-ai/dsh-settings"; // src/agent-preset-install.ts import { cpSync, - existsSync, + existsSync as existsSync2, mkdirSync, - readFileSync, + readFileSync as readFileSync2, rmSync, - writeFileSync + writeFileSync as writeFileSync2 } from "node:fs"; import { createRequire as createRequire2 } from "node:module"; -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"; -var NETXOPS_PRESET_ID = "netxops"; + +// src/netx/heal-profile-entry.ts +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; var DESKTOP_PATCH_BEGIN = "# BEGIN dsh-netxops-preset (managed)"; var DESKTOP_PATCH_END = "# END dsh-netxops-preset (managed)"; -var PATCH_PLUGIN_INDENT = " "; -function packageRoot() { - return join(dirname(fileURLToPath(import.meta.url)), ".."); -} +var NETXOPS_HOST_CONFIG_DEFAULTS = Object.freeze({ + apiUrl: "http://127.0.0.1:8890", + lang: "zh", + thinkingLanguage: "auto", + replyLanguage: "follow-user", + nmsProvider: "zte-ume", + tokenCredentialRef: "NETX_API_TOKEN", + toolCallTimeoutMs: 120000, + installAgentPreset: true, + alarmPushEnabled: false, + alarmDeliverDsh: true, + alarmDeliverIm: false, + imBotId: "", + imTargetId: "", + imTargets: "", + groupOpsInPreset: true, + groupOpsPublic: true, + groupTopologyInPreset: false, + groupTopologyPublic: false, + groupBizMonitorInPreset: true, + groupBizMonitorPublic: false, + kbRoot: "", + groupKbInPreset: true, + groupKbPublic: false +}); function resolveDshHome() { const fromEnv = process.env.DSH_HOME?.trim(); if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv; return join(homedir(), ".dsh"); } +function managedLineMask(lines) { + const skip = lines.map(() => false); + let inside = false; + for (let i = 0;i < lines.length; i += 1) { + const line = lines[i]; + if (line.includes(DESKTOP_PATCH_BEGIN)) + inside = true; + if (inside) + skip[i] = true; + if (line.includes(DESKTOP_PATCH_END)) + inside = false; + } + return skip; +} +function parseScalar(raw) { + const text = raw.trim(); + if (text === "true") + return true; + if (text === "false") + return false; + if (/^-?\d+$/.test(text)) + return Number(text); + if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) { + return text.slice(1, -1); + } + return text; +} +function formatScalar(value) { + if (typeof value === "boolean" || typeof value === "number") + return String(value); + if (value === "") + return "''"; + if (/[:#\n\r]|^\s|\s$/.test(value) || value.includes("'")) { + return JSON.stringify(value); + } + return value; +} +function findHostEntry(lines, skip) { + for (let i = 0;i < lines.length; i += 1) { + if (skip[i]) + continue; + if (lines[i] !== "- id: netxops") + continue; + let end = i + 1; + while (end < lines.length) { + const line = lines[end]; + if (skip[end]) + break; + if (/^- id:/.test(line)) + break; + end += 1; + } + while (end > i + 1 && lines[end - 1].trim() === "") + end -= 1; + let disabled; + let name; + const config = {}; + let inConfig = false; + for (let j = i + 1;j < end; j += 1) { + const line = lines[j]; + const disabledMatch = /^ disabled:\s*(.+)\s*$/.exec(line); + if (disabledMatch) { + disabled = parseScalar(disabledMatch[1]) === true; + inConfig = false; + continue; + } + const nameMatch = /^ name:\s*(.+)\s*$/.exec(line); + if (nameMatch) { + name = String(parseScalar(nameMatch[1])); + inConfig = false; + continue; + } + if (/^ config:\s*$/.test(line)) { + inConfig = true; + continue; + } + if (inConfig) { + const cfg = /^ ([A-Za-z0-9_]+):\s*(.*?)\s*$/.exec(line); + if (cfg) { + config[cfg[1]] = parseScalar(cfg[2]); + continue; + } + if (/^ \S/.test(line)) + inConfig = false; + } + } + return { start: i, end, disabled, name, config }; + } + return null; +} +function needsHeal(entry) { + if (entry.name !== "dsh-netxops") + return true; + for (const key of Object.keys(NETXOPS_HOST_CONFIG_DEFAULTS)) { + if (!Object.hasOwn(entry.config, key)) + return true; + } + return false; +} +function renderHostEntry(entry) { + const config = { ...NETXOPS_HOST_CONFIG_DEFAULTS, ...entry.config }; + const out = [ + "- id: netxops", + " name: dsh-netxops" + ]; + if (entry.disabled === true) + out.push(" disabled: true"); + else if (entry.disabled === false) + out.push(" disabled: false"); + out.push(" config:"); + for (const [key, value] of Object.entries(config)) { + out.push(` ${key}: ${formatScalar(value)}`); + } + return out; +} +function healNetxopsProfileEntries(logger) { + let rewritten = 0; + for (const profileName of ["desktop", "web"]) { + const patchPath = join(resolveDshHome(), "profiles", profileName, "cordis.patch.yml"); + if (!existsSync(patchPath)) + continue; + const raw = readFileSync(patchPath, "utf8"); + const nl = raw.includes(`\r +`) ? `\r +` : ` +`; + const lines = raw.replace(/\r\n/g, ` +`).split(` +`); + const skip = managedLineMask(lines); + const entry = findHostEntry(lines, skip); + if (entry === null) + continue; + if (!needsHeal(entry)) + continue; + const rendered = renderHostEntry(entry); + const next = [ + ...lines.slice(0, entry.start), + ...rendered, + ...lines.slice(entry.end) + ].join(nl); + const normalized = next.endsWith(nl) ? next : `${next}${nl}`; + if (normalized === raw) + continue; + writeFileSync(patchPath, normalized, "utf8"); + rewritten += 1; + logger.info("netxops: healed sparse host entry in profiles/%s/cordis.patch.yml (restart %s to load capability defaults)", profileName, profileName === "desktop" ? "Desktop" : "dsh web"); + } + return rewritten; +} + +// src/agent-preset-install.ts +var NETXOPS_PRESET_ID = "netxops"; +var DESKTOP_PATCH_BEGIN2 = "# BEGIN dsh-netxops-preset (managed)"; +var DESKTOP_PATCH_END2 = "# END dsh-netxops-preset (managed)"; +var PATCH_PLUGIN_INDENT = " "; +function packageRoot() { + return join2(dirname(fileURLToPath(import.meta.url)), ".."); +} +function resolveDshHome2() { + const fromEnv = process.env.DSH_HOME?.trim(); + if (fromEnv !== undefined && fromEnv.length > 0) + return fromEnv; + return join2(homedir2(), ".dsh"); +} function electronResourcesRoots() { const roots = []; const rp = process.resourcesPath; if (typeof rp === "string" && rp.length > 0) roots.push(rp); - roots.push(join(dirname(process.execPath), "resources")); + roots.push(join2(dirname(process.execPath), "resources")); return roots; } function readableFile(path) { try { - readFileSync(path, { encoding: "utf8", flag: "r" }); + readFileSync2(path, { encoding: "utf8", flag: "r" }); return true; } catch { return false; } } function resolveDesktopStandardPatchPath() { - const dshHome = resolveDshHome(); + const dshHome = resolveDshHome2(); const envOverride = process.env.NETXOPS_STANDARD_PATCH?.trim(); const candidates = []; if (envOverride) candidates.push(envOverride); for (const resources of electronResourcesRoots()) { - candidates.push(join(resources, "app.asar", "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", "standard.patch.yml"), join(resources, "app.asar.unpacked", "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", "standard.patch.yml")); + candidates.push(join2(resources, "app.asar", "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", "standard.patch.yml"), join2(resources, "app.asar.unpacked", "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", "standard.patch.yml")); } const requireAnchors = [ - join(dshHome, "profiles", "desktop", "package.json"), - join(dshHome, "profiles", "desktop", "node_modules", "dsh-netxops", "package.json"), - join(packageRoot(), "package.json"), - join(dshHome, "profiles", "node_modules", "@deepseek-ai", "dsh-web-app", "package.json"), - join(dshHome, "profiles", "web", "package.json") + join2(dshHome, "profiles", "desktop", "package.json"), + join2(dshHome, "profiles", "desktop", "node_modules", "dsh-netxops", "package.json"), + join2(packageRoot(), "package.json"), + join2(dshHome, "profiles", "node_modules", "@deepseek-ai", "dsh-web-app", "package.json"), + join2(dshHome, "profiles", "web", "package.json") ]; for (const anchor of requireAnchors) { - if (!existsSync(anchor)) + if (!existsSync2(anchor)) continue; try { const req = createRequire2(anchor); @@ -73,7 +263,7 @@ function resolveDesktopStandardPatchPath() { candidates.push(req.resolve("@deepseek-ai/dsh-web-app/presets/standard.patch.yml")); } catch {} const pkg = dirname(req.resolve("@deepseek-ai/dsh-web-app/package.json")); - candidates.push(join(pkg, "presets", "standard.patch.yml")); + candidates.push(join2(pkg, "presets", "standard.patch.yml")); } catch {} } for (const path of candidates) { @@ -83,38 +273,38 @@ function resolveDesktopStandardPatchPath() { return null; } function resolveStandardPresetDir() { - const dshHome = resolveDshHome(); + const dshHome = resolveDshHome2(); const candidates = [ - join(dshHome, "profiles", "desktop", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"), - join(dshHome, "profiles", "web", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"), - join(dirname(fileURLToPath(import.meta.url)), "..", "..", "DeepSeekHarness", "packages", "preset", "agent-presets", "presets", "standard"), - join(dshHome, "profiles", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard") + join2(dshHome, "profiles", "desktop", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"), + join2(dshHome, "profiles", "web", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"), + join2(dirname(fileURLToPath(import.meta.url)), "..", "..", "DeepSeekHarness", "packages", "preset", "agent-presets", "presets", "standard"), + join2(dshHome, "profiles", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard") ]; for (const dir of candidates) { - if (existsSync(join(dir, "agent.cordis.yml"))) + if (existsSync2(join2(dir, "agent.cordis.yml"))) return dir; } for (const nm of [ - join(dshHome, "profiles", "desktop", "node_modules"), - join(dshHome, "profiles", "web", "node_modules"), - join(dshHome, "profiles", "node_modules") + join2(dshHome, "profiles", "desktop", "node_modules"), + join2(dshHome, "profiles", "web", "node_modules"), + join2(dshHome, "profiles", "node_modules") ]) { - const pkgJson = join(nm, "@deepseek-ai", "dsh-agent-presets", "package.json"); - if (!existsSync(pkgJson)) + const pkgJson = join2(nm, "@deepseek-ai", "dsh-agent-presets", "package.json"); + if (!existsSync2(pkgJson)) continue; try { const req = createRequire2(pkgJson); const root = dirname(req.resolve("@deepseek-ai/dsh-agent-presets/package.json")); - const dir = join(root, "presets", "standard"); - if (existsSync(join(dir, "agent.cordis.yml"))) + const dir = join2(root, "presets", "standard"); + if (existsSync2(join2(dir, "agent.cordis.yml"))) return dir; } catch {} } try { const req = createRequire2(import.meta.url); const root = dirname(req.resolve("@deepseek-ai/dsh-agent-presets/package.json")); - const dir = join(root, "presets", "standard"); - if (existsSync(join(dir, "agent.cordis.yml"))) + const dir = join2(root, "presets", "standard"); + if (existsSync2(join2(dir, "agent.cordis.yml"))) return dir; } catch {} return null; @@ -123,31 +313,31 @@ function desktopRuntimeLikelyPresent() { if (typeof process.versions.electron === "string") return true; for (const resources of electronResourcesRoots()) { - if (existsSync(join(resources, "app.asar"))) + if (existsSync2(join2(resources, "app.asar"))) return true; } const localAppData = process.env.LOCALAPPDATA?.trim(); if (localAppData) { - if (existsSync(join(localAppData, "Programs", "DeepSeek Harness", "resources", "app.asar"))) { + if (existsSync2(join2(localAppData, "Programs", "DeepSeek Harness", "resources", "app.asar"))) { return true; } } return false; } function hostResolveBases() { - const dshHome = resolveDshHome(); + const dshHome = resolveDshHome2(); const bases = []; for (const resources of electronResourcesRoots()) { - bases.push(join(resources, "app.asar", "dsh", "package.json")); - bases.push(join(resources, "app.asar", "dsh", "node_modules")); + bases.push(join2(resources, "app.asar", "dsh", "package.json")); + bases.push(join2(resources, "app.asar", "dsh", "node_modules")); } const localAppData = process.env.LOCALAPPDATA?.trim(); if (localAppData) { - const asarDsh = join(localAppData, "Programs", "DeepSeek Harness", "resources", "app.asar", "dsh"); - bases.push(join(asarDsh, "package.json"), join(asarDsh, "node_modules")); + const asarDsh = join2(localAppData, "Programs", "DeepSeek Harness", "resources", "app.asar", "dsh"); + bases.push(join2(asarDsh, "package.json"), join2(asarDsh, "node_modules")); } - bases.push(join(dshHome, "profiles", "desktop", "package.json"), join(dshHome, "profiles", "desktop", "node_modules"), join(dshHome, "profiles", "web", "package.json"), join(dshHome, "profiles", "web", "node_modules"), join(dshHome, "profiles", "node_modules"), join(packageRoot(), "package.json")); - return [...new Set(bases.filter((path) => path.includes("app.asar") || existsSync(path)))]; + bases.push(join2(dshHome, "profiles", "desktop", "package.json"), join2(dshHome, "profiles", "desktop", "node_modules"), join2(dshHome, "profiles", "web", "package.json"), join2(dshHome, "profiles", "web", "node_modules"), join2(dshHome, "profiles", "node_modules"), join2(packageRoot(), "package.json")); + return [...new Set(bases.filter((path) => path.includes("app.asar") || existsSync2(path)))]; } function packageInstalledAbove(name, bases) { if (name.startsWith("cordis:") || name.startsWith("node:")) @@ -156,16 +346,16 @@ function packageInstalledAbove(name, bases) { for (const base of bases) { let dir = base.endsWith("package.json") ? dirname(base) : base; for (;; ) { - if (readableFile(join(dir, "node_modules", pkg, "package.json"))) + if (readableFile(join2(dir, "node_modules", pkg, "package.json"))) return true; - if (readableFile(join(dir, pkg, "package.json"))) + if (readableFile(join2(dir, pkg, "package.json"))) return true; const parent = dirname(dir); if (parent === dir) break; dir = parent; } - const requireAnchor = base.endsWith("package.json") ? base : existsSync(join(base, "package.json")) ? join(base, "package.json") : null; + const requireAnchor = base.endsWith("package.json") ? base : existsSync2(join2(base, "package.json")) ? join2(base, "package.json") : null; if (requireAnchor !== null) { try { createRequire2(requireAnchor).resolve(`${pkg}/package.json`); @@ -339,7 +529,7 @@ function personaEntryFromMarkdown(md) { `); } function readPresetMeta(metaPath) { - const metaText = readFileSync(metaPath, "utf8"); + const metaText = readFileSync2(metaPath, "utf8"); return { name: (metaText.match(/^name:\s*(.+)$/m) || [])[1]?.trim() || "Netx Ops", description: (metaText.match(/^description:\s*(.+)$/m) || [])[1]?.trim() || "", @@ -347,13 +537,13 @@ function readPresetMeta(metaPath) { }; } function desktopProfilePresent() { - return existsSync(join(resolveDshHome(), "profiles", "desktop")); + return existsSync2(join2(resolveDshHome2(), "profiles", "desktop")); } function composeNetxopsPresetArtifacts() { - const overlayRoot = join(packageRoot(), "presets", NETXOPS_PRESET_ID); - const personaPath = join(overlayRoot, "PERSONA.md"); - const metaPath = join(overlayRoot, "preset.yml"); - if (!existsSync(personaPath) || !existsSync(metaPath)) { + const overlayRoot = join2(packageRoot(), "presets", NETXOPS_PRESET_ID); + const personaPath = join2(overlayRoot, "PERSONA.md"); + const metaPath = join2(overlayRoot, "preset.yml"); + if (!existsSync2(personaPath) || !existsSync2(metaPath)) { throw new Error(`bundled preset overlay missing under ${overlayRoot}`); } const meta = readPresetMeta(metaPath); @@ -368,7 +558,7 @@ function composeNetxopsPresetArtifacts() { standardSource = patchPath; standardKind = "desktop-patch"; standardDir = null; - agentCordis = pluginsBodyToAgentCordis(extractPluginsBodyFromStandardPatch(readFileSync(patchPath, "utf8"))); + agentCordis = pluginsBodyToAgentCordis(extractPluginsBodyFromStandardPatch(readFileSync2(patchPath, "utf8"))); } else { const dir = resolveStandardPresetDir(); if (dir === null) { @@ -377,9 +567,9 @@ function composeNetxopsPresetArtifacts() { standardSource = dir; standardKind = "directory"; standardDir = dir; - agentCordis = readFileSync(join(dir, "agent.cordis.yml"), "utf8"); + agentCordis = readFileSync2(join2(dir, "agent.cordis.yml"), "utf8"); } - agentCordis = replaceTopLevelEntry(agentCordis, "persona", personaEntryFromMarkdown(readFileSync(personaPath, "utf8"))); + agentCordis = replaceTopLevelEntry(agentCordis, "persona", personaEntryFromMarkdown(readFileSync2(personaPath, "utf8"))); agentCordis = ensureNetxopsToolsRow(agentCordis); agentCordis = alignWorkflowEngineForHost(agentCordis, bases); if (!agentCordis.endsWith(` @@ -411,23 +601,23 @@ ${pluginsBody} return { standardSource, standardKind, standardDir, ...meta, agentCordis, declarativePatch }; } function installDirectoryPreset(artifacts, logger) { - const overlayRoot = join(packageRoot(), "presets", NETXOPS_PRESET_ID); - const destParent = join(resolveDshHome(), ".agent-presets"); - const dest = join(destParent, NETXOPS_PRESET_ID); + const overlayRoot = join2(packageRoot(), "presets", NETXOPS_PRESET_ID); + const destParent = join2(resolveDshHome2(), ".agent-presets"); + const dest = join2(destParent, NETXOPS_PRESET_ID); mkdirSync(destParent, { recursive: true }); - if (existsSync(dest)) + if (existsSync2(dest)) rmSync(dest, { recursive: true, force: true }); mkdirSync(dest, { recursive: true }); - if (artifacts.standardDir !== null && existsSync(artifacts.standardDir)) { + if (artifacts.standardDir !== null && existsSync2(artifacts.standardDir)) { cpSync(artifacts.standardDir, dest, { recursive: true }); } - writeFileSync(join(dest, "preset.yml"), readFileSync(join(overlayRoot, "preset.yml"), "utf8")); - writeFileSync(join(dest, "PERSONA.md"), readFileSync(join(overlayRoot, "PERSONA.md"), "utf8")); - writeFileSync(join(dest, "agent.cordis.yml"), artifacts.agentCordis); - const skillsSrc = join(overlayRoot, "skills"); - if (existsSync(skillsSrc)) - cpSync(skillsSrc, join(dest, "skills"), { recursive: true }); - writeFileSync(join(dest, ".dsh-netxops-managed"), [ + writeFileSync2(join2(dest, "preset.yml"), readFileSync2(join2(overlayRoot, "preset.yml"), "utf8")); + writeFileSync2(join2(dest, "PERSONA.md"), readFileSync2(join2(overlayRoot, "PERSONA.md"), "utf8")); + writeFileSync2(join2(dest, "agent.cordis.yml"), artifacts.agentCordis); + const skillsSrc = join2(overlayRoot, "skills"); + if (existsSync2(skillsSrc)) + cpSync(skillsSrc, join2(dest, "skills"), { recursive: true }); + writeFileSync2(join2(dest, ".dsh-netxops-managed"), [ `composed-from: ${artifacts.standardKind}`, `standard-source: ${artifacts.standardSource}`, `at: ${new Date().toISOString()}`, @@ -439,11 +629,11 @@ function installDirectoryPreset(artifacts, logger) { function syncDeclarativeIntoProfilePatch(profileName, declarativePatch, logger) { if (profileName !== "desktop" && profileName !== "web") return; - const profileDir = join(resolveDshHome(), "profiles", profileName); - const patchPath = join(profileDir, "cordis.patch.yml"); - if (!existsSync(profileDir)) + const profileDir = join2(resolveDshHome2(), "profiles", profileName); + const patchPath = join2(profileDir, "cordis.patch.yml"); + if (!existsSync2(profileDir)) return; - let patch = existsSync(patchPath) ? readFileSync(patchPath, "utf8") : `[] + let patch = existsSync2(patchPath) ? readFileSync2(patchPath, "utf8") : `[] `; patch = stripManagedPresetBlocks(patch); patch = patch.replace(/^- id: preset-netxops\n(?: .*\n)*/m, ""); @@ -453,25 +643,25 @@ function syncDeclarativeIntoProfilePatch(profileName, declarativePatch, logger) `) && patch.length > 0) patch += ` `; - const managed = `${DESKTOP_PATCH_BEGIN} + const managed = `${DESKTOP_PATCH_BEGIN2} ${declarativePatch.trimEnd()} -${DESKTOP_PATCH_END} +${DESKTOP_PATCH_END2} `; - writeFileSync(patchPath, `${patch}${managed}`, "utf8"); + writeFileSync2(patchPath, `${patch}${managed}`, "utf8"); logger.info("netxops: declarative preset synced into profiles/%s/cordis.patch.yml (restart %s to load)", profileName, profileName === "desktop" ? "Desktop" : "dsh web"); } function stripManagedPresetBlocks(patch) { let out = patch; for (;; ) { - const start = out.indexOf(DESKTOP_PATCH_BEGIN); + const start = out.indexOf(DESKTOP_PATCH_BEGIN2); if (start < 0) break; - const end = out.indexOf(DESKTOP_PATCH_END, start); + const end = out.indexOf(DESKTOP_PATCH_END2, start); if (end < 0) { out = out.slice(0, start); break; } - let cut = end + DESKTOP_PATCH_END.length; + let cut = end + DESKTOP_PATCH_END2.length; if (out[cut] === "\r") cut += 1; if (out[cut] === ` @@ -495,7 +685,7 @@ function ensureAgentPresetInstalled(logger) { logger.error("netxops: failed to install directory preset: %s", error); } try { - writeFileSync(join(packageRoot(), "presets", "netxops.preset.patch.yml"), artifacts.declarativePatch); + writeFileSync2(join2(packageRoot(), "presets", "netxops.preset.patch.yml"), artifacts.declarativePatch); } catch {} try { for (const profile of ["desktop", "web"]) { @@ -504,6 +694,11 @@ function ensureAgentPresetInstalled(logger) { } catch (error) { logger.error("netxops: failed to sync declarative preset into profile patch: %s", error); } + try { + healNetxopsProfileEntries(logger); + } catch (error) { + logger.error("netxops: failed to heal host profile entry: %s", error); + } } // src/netx/alarm-push-status.ts var STORE_KEY = Symbol.for("dsh-netxops.alarm-push-status"); @@ -899,16 +1094,16 @@ async function deliverAlarmToIm(ctx, payload, options) { } // src/netx/alarm-session.ts -import { homedir as homedir2 } from "node:os"; -import { join as join2 } from "node:path"; +import { homedir as homedir3 } from "node:os"; +import { join as join3 } from "node:path"; var PRESET_ID = "netxops"; var PERMISSION_PRESET = "default"; var sticky = null; var deliveryChain = Promise.resolve(); function resolveWorkspacePath() { const fromEnv = process.env.DSH_HOME?.trim(); - const home = fromEnv && fromEnv.length > 0 ? fromEnv : join2(homedir2(), ".dsh"); - return join2(home, "workspaces", "netxops-alarms"); + const home = fromEnv && fromEnv.length > 0 ? fromEnv : join3(homedir3(), ".dsh"); + return join3(home, "workspaces", "netxops-alarms"); } async function disposeSticky(handle) { if (!handle) @@ -1066,7 +1261,7 @@ async function dispatchAlarmToSinks(ctx, payload, options, sinks = {}) { // src/netx/capability-groups.ts var DEFAULT_CAPABILITY_GROUPS = Object.freeze({ - ops: Object.freeze({ inPreset: true, public: false }), + ops: Object.freeze({ inPreset: true, public: true }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }) }); @@ -1149,7 +1344,18 @@ function capabilityGroupsFromSettings(fields) { return { ops: { inPreset: opsInPreset, - public: src.groupOpsPublic === true || src.groupNmsPublic === true || src.groupCommonPublic === true || src.groupManagedNePublic === true + public: (() => { + if (src.groupOpsPublic !== undefined) + return src.groupOpsPublic !== false; + const legacy = [ + src.groupNmsPublic, + src.groupCommonPublic, + src.groupManagedNePublic + ].filter((v) => v !== undefined); + if (legacy.length === 0) + return true; + return legacy.some((v) => v === true); + })() }, topology: { inPreset: src.groupTopologyInPreset === true || src.groupTopologyLayoutInPreset === true, @@ -1179,29 +1385,29 @@ function toolNamesForGroups(groupIds) { } // src/netx/group-skills.ts -import { existsSync as existsSync2 } from "node:fs"; +import { existsSync as existsSync3 } from "node:fs"; import { readdir, readFile, stat } from "node:fs/promises"; -import { dirname as dirname2, join as join3 } from "node:path"; +import { dirname as dirname2, join as join4 } from "node:path"; import { fileURLToPath as fileURLToPath2 } from "node:url"; function opsSkillsRoot() { const envRoot = process.env.NETX_SKILLS_ROOT?.trim(); - if (envRoot && existsSync2(envRoot)) + if (envRoot && existsSync3(envRoot)) return envRoot; const here = dirname2(fileURLToPath2(import.meta.url)); const siblingCandidates = [ - join3(here, "..", "..", "..", "netx", "skills"), - join3(here, "..", "..", "netx", "skills") + join4(here, "..", "..", "..", "netx", "skills"), + join4(here, "..", "..", "netx", "skills") ]; for (const candidate of siblingCandidates) { - if (existsSync2(candidate)) + if (existsSync3(candidate)) return candidate; } const packaged = [ - join3(here, "..", "presets", "netxops", "skills"), - join3(here, "..", "..", "presets", "netxops", "skills") + join4(here, "..", "presets", "netxops", "skills"), + join4(here, "..", "..", "presets", "netxops", "skills") ]; for (const candidate of packaged) { - if (existsSync2(candidate)) + if (existsSync3(candidate)) return candidate; } return packaged[0]; @@ -1252,7 +1458,7 @@ function stripQuotes(value) { return value; } async function loadSkillBundle(dir) { - const skillPath = join3(dir, "SKILL.md"); + const skillPath = join4(dir, "SKILL.md"); let raw; try { raw = await readFile(skillPath, "utf8"); @@ -1275,7 +1481,7 @@ async function loadSkillBundle(dir) { }; } async function loadGroupSkills(skillsRoot, groupId) { - const groupDir = join3(skillsRoot, SKILL_DIR_BY_GROUP[groupId]); + const groupDir = join4(skillsRoot, SKILL_DIR_BY_GROUP[groupId]); let entries; try { entries = await readdir(groupDir); @@ -1285,7 +1491,7 @@ async function loadGroupSkills(skillsRoot, groupId) { const skills = []; const skipped = []; for (const entry of entries) { - const full = join3(groupDir, entry); + const full = join4(groupDir, entry); let isDir = false; try { isDir = (await stat(full)).isDirectory(); @@ -1294,7 +1500,7 @@ async function loadGroupSkills(skillsRoot, groupId) { } if (!isDir) continue; - const skillPath = join3(full, "SKILL.md"); + const skillPath = join4(full, "SKILL.md"); let raw; try { raw = await readFile(skillPath, "utf8"); @@ -1343,7 +1549,7 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) { log?.warn?.("%s: skip skill dir %s (%s)", providerLabel, miss.directory, miss.reason); } if (skills.length === 0) { - log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join3(root, SKILL_DIR_BY_GROUP[groupId])); + log?.warn?.("%s: no skills loaded for group=%s from %s", providerLabel, groupId, join4(root, SKILL_DIR_BY_GROUP[groupId])); } for (const skill of skills) { disposers.push(skillsApi.register({ @@ -1364,8 +1570,8 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) { } // src/netx/kb-local-path.ts -import { existsSync as existsSync3, realpathSync, statSync } from "node:fs"; -import { join as join4, relative, resolve, sep } from "node:path"; +import { existsSync as existsSync4, realpathSync, statSync } from "node:fs"; +import { join as join5, relative, resolve, sep } from "node:path"; var KB_LOCAL_WRITABLE_ROOTS = Object.freeze([ "memories", "drafts", @@ -1476,7 +1682,7 @@ function resolveExistingWritableFile(localRoot, pathArg) { } const checked = resolveWritableLocalPath(localRoot, ...relative(localRoot, absolutePath).split(/[/\\]/).filter(Boolean)); try { - if (existsSync3(checked.absolutePath)) { + if (existsSync4(checked.absolutePath)) { const realFile = realpathSync(checked.absolutePath); const realLocal = realpathSync(localRoot); const realRel = relative(realLocal, realFile); @@ -1504,16 +1710,16 @@ function resolveExistingWritableFile(localRoot, pathArg) { return checked; } function memoryDir(bucket) { - return join4("memories", MEMORY_BUCKET_DIR[bucket]); + return join5("memories", MEMORY_BUCKET_DIR[bucket]); } function suggestionDir(kind) { - return join4("suggestions", kind); + return join5("suggestions", kind); } function draftDir(domain) { const d = domain?.trim(); if (!d) return "drafts"; - return join4("drafts", sanitizeSlug(d, "misc")); + return join5("drafts", sanitizeSlug(d, "misc")); } function refDir(area, device) { if (!REF_AREAS.includes(area)) { @@ -1524,9 +1730,9 @@ function refDir(area, device) { if (!host) { throw new Error("refs/devices requires device (host_name)"); } - return join4("refs", "devices", host); + return join5("refs", "devices", host); } - return join4("refs", area); + return join5("refs", area); } // src/netx/kb-context-skill.ts @@ -1619,8 +1825,8 @@ function registerKbContextSkill(ctx, snapshot) { } // src/netx/kb-manifest.ts -import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs"; -import { basename, dirname as dirname3, join as join5, resolve as resolve2 } from "node:path"; +import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs"; +import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "node:path"; var EMPTY_CONTENT = { hasRegions: false, hasTheory: false, @@ -1673,8 +1879,8 @@ function findManifest(kbRoot, maxDepth = 3) { if (!rootStat.isDirectory()) { return { paths: [], error: `kbRoot is not a directory: ${root}` }; } - const direct = join5(root, "MANIFEST.json"); - if (existsSync4(direct)) { + const direct = join6(root, "MANIFEST.json"); + if (existsSync5(direct)) { try { if (statSync2(direct).isFile()) return { paths: [direct] }; @@ -1684,8 +1890,8 @@ function findManifest(kbRoot, maxDepth = 3) { const walk = (dir, depth) => { if (depth > maxDepth) return; - const candidate = join5(dir, "MANIFEST.json"); - if (existsSync4(candidate)) { + const candidate = join6(dir, "MANIFEST.json"); + if (existsSync5(candidate)) { try { if (statSync2(candidate).isFile()) found.push(candidate); @@ -1702,7 +1908,7 @@ function findManifest(kbRoot, maxDepth = 3) { for (const name of entries) { if (name === "node_modules" || name === ".git") continue; - const full = join5(dir, name); + const full = join6(dir, name); let st; try { st = statSync2(full); @@ -1722,7 +1928,7 @@ function findManifest(kbRoot, maxDepth = 3) { for (const name of topEntries) { if (name === "node_modules" || name === ".git") continue; - const full = join5(root, name); + const full = join6(root, name); let st; try { st = statSync2(full); @@ -1840,7 +2046,7 @@ function resolveKbRoot(kbRoot, maxDepth = 3) { const manifestPath = located.paths[0]; let raw; try { - raw = readFileSync2(manifestPath, "utf8"); + raw = readFileSync3(manifestPath, "utf8"); } catch (error) { return errorSnapshot(`cannot read MANIFEST: ${error instanceof Error ? error.message : String(error)}`); } @@ -1866,15 +2072,15 @@ import { defineTool } from "@deepseek-ai/dsh-tools"; // src/netx/kb-local-ops.ts import { - existsSync as existsSync5, + existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync2, - readFileSync as readFileSync3, + readFileSync as readFileSync4, rmSync as rmSync2, statSync as statSync3, - writeFileSync as writeFileSync2 + writeFileSync as writeFileSync3 } from "node:fs"; -import { dirname as dirname4, join as join6 } from "node:path"; +import { dirname as dirname4, join as join7 } from "node:path"; function requireLocalRoot(snapshot) { const root = resolveKbLocalRoot(snapshot); if (!root) { @@ -1920,12 +2126,12 @@ function createMemory(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = memoryFileName({ date: args.date, slug: args.slug }); const target = resolveWritableLocalPath(localRoot, memoryDir(args.bucket), name); - if (existsSync5(target.absolutePath) && !args.overwrite) { + if (existsSync6(target.absolutePath) && !args.overwrite) { throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); } mkdirSync2(dirname4(target.absolutePath), { recursive: true }); - const existed = existsSync5(target.absolutePath); - writeFileSync2(target.absolutePath, args.body.endsWith(` + const existed = existsSync6(target.absolutePath); + writeFileSync3(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1939,12 +2145,12 @@ function createDraft(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = draftFileName({ date: args.date, slug: args.slug }); const target = resolveWritableLocalPath(localRoot, draftDir(args.domain), name); - if (existsSync5(target.absolutePath) && !args.overwrite) { + if (existsSync6(target.absolutePath) && !args.overwrite) { throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); } mkdirSync2(dirname4(target.absolutePath), { recursive: true }); - const existed = existsSync5(target.absolutePath); - writeFileSync2(target.absolutePath, ensureDraftBody(args.body), "utf8"); + const existed = existsSync6(target.absolutePath); + writeFileSync3(target.absolutePath, ensureDraftBody(args.body), "utf8"); return { ok: true, action: existed ? "updated" : "created", @@ -1956,12 +2162,12 @@ function createSuggestion(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = suggestionFileName({ date: args.date, slug: args.slug }); const target = resolveWritableLocalPath(localRoot, suggestionDir(args.kind), name); - if (existsSync5(target.absolutePath) && !args.overwrite) { + if (existsSync6(target.absolutePath) && !args.overwrite) { throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); } mkdirSync2(dirname4(target.absolutePath), { recursive: true }); - const existed = existsSync5(target.absolutePath); - writeFileSync2(target.absolutePath, args.body.endsWith(` + const existed = existsSync6(target.absolutePath); + writeFileSync3(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1975,12 +2181,12 @@ function createRef(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const name = refFileName(args.slug); const target = resolveWritableLocalPath(localRoot, refDir(args.area, args.device), name); - if (existsSync5(target.absolutePath) && !args.overwrite) { + if (existsSync6(target.absolutePath) && !args.overwrite) { throw new Error(`file already exists (pass overwrite=true to replace): ${target.relativePath}`); } mkdirSync2(dirname4(target.absolutePath), { recursive: true }); - const existed = existsSync5(target.absolutePath); - writeFileSync2(target.absolutePath, args.body.endsWith(` + const existed = existsSync6(target.absolutePath); + writeFileSync3(target.absolutePath, args.body.endsWith(` `) ? args.body : `${args.body} `, "utf8"); return { @@ -1993,13 +2199,13 @@ function createRef(snapshot, args) { function updateLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync5(target.absolutePath)) { + if (!existsSync6(target.absolutePath)) { throw new Error(`file not found: ${target.relativePath}`); } const body = target.writableRoot === "drafts" ? ensureDraftBody(args.body) : args.body.endsWith(` `) ? args.body : `${args.body} `; - writeFileSync2(target.absolutePath, body, "utf8"); + writeFileSync3(target.absolutePath, body, "utf8"); return { ok: true, action: "updated", @@ -2010,7 +2216,7 @@ function updateLocalFile(snapshot, args) { function deleteLocalFile(snapshot, args) { const localRoot = requireLocalRoot(snapshot); const target = resolveExistingWritableFile(localRoot, args.path); - if (!existsSync5(target.absolutePath)) { + if (!existsSync6(target.absolutePath)) { throw new Error(`file not found: ${target.relativePath}`); } rmSync2(target.absolutePath, { force: false }); @@ -2031,7 +2237,7 @@ function walkFiles(dir, baseLocal, out) { for (const name of entries) { if (name === "." || name === "..") continue; - const full = join6(dir, name); + const full = join7(dir, name); let st; try { st = statSync3(full); @@ -2056,7 +2262,7 @@ function listLocalFiles(snapshot, args = {}) { const roots = args.root && args.root !== "all" ? [args.root] : [...KB_LOCAL_WRITABLE_ROOTS]; const entries = []; for (const r of roots) { - walkFiles(join6(localRoot, r), localRoot, entries); + walkFiles(join7(localRoot, r), localRoot, entries); } entries.sort((a, b) => b.mtimeMs - a.mtimeMs); const limit = Math.min(Math.max(args.limit ?? 50, 1), 200); @@ -2304,19 +2510,19 @@ function registerKbLocalTools(ctx, snapshot = getKbContext()) { // src/netx/kb-pack-skills.ts import { readdir as readdir2, stat as stat2 } from "node:fs/promises"; -import { join as join7 } from "node:path"; +import { join as join8 } from "node:path"; function kbPackSkillsEnabled(snapshot) { return snapshot.status === "configured" && snapshot.content.hasSkills === true; } function resolveKbSkillsRoot(snapshot) { const rel = snapshot.paths.skills?.trim() || "_skills"; - return join7(snapshot.realRoot, rel); + return join8(snapshot.realRoot, rel); } function resolveKbLocalSkillsRoot(snapshot) { const rel = snapshot.paths.localSkills?.trim(); if (!rel) return null; - return join7(snapshot.realRoot, rel); + return join8(snapshot.realRoot, rel); } async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) { let entries; @@ -2332,7 +2538,7 @@ async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) return []; const disposers = []; for (const entry of entries) { - const full = join7(root, entry); + const full = join8(root, entry); let isDir = false; try { isDir = (await stat2(full)).isDirectory(); @@ -2546,7 +2752,7 @@ function watchNetxConnection(listener) { } // src/netx/session-export.ts -import { homedir as homedir3, hostname as osHostname } from "node:os"; +import { homedir as homedir4, hostname as osHostname } from "node:os"; // node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs import { createRequire as createRequire3 } from "module"; @@ -5788,7 +5994,7 @@ var Config = z.object({ imTargetId: vol(z.string().default("")), nmsProvider: vol(z.string().default("zte-ume")), groupOpsInPreset: vol(z.boolean().default(true)), - groupOpsPublic: vol(z.boolean().default(false)), + groupOpsPublic: vol(z.boolean().default(true)), groupTopologyInPreset: vol(z.boolean().default(false)), groupTopologyPublic: vol(z.boolean().default(false)), groupBizMonitorInPreset: vol(z.boolean().default(true)), diff --git a/package.json b/package.json index f01ac27..c596e15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-netxops", - "version": "0.1.50", + "version": "0.1.51", "description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills", "license": "MIT", "type": "module", diff --git a/src/agent-preset-install.ts b/src/agent-preset-install.ts index 4e2edb8..13c0df2 100644 --- a/src/agent-preset-install.ts +++ b/src/agent-preset-install.ts @@ -27,6 +27,7 @@ import { homedir } from 'node:os' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' import type { Context } from '@deepseek-ai/cordis' +import { healNetxopsProfileEntries } from './netx/heal-profile-entry.ts' export const NETXOPS_PRESET_ID = 'netxops' @@ -610,4 +611,12 @@ export function ensureAgentPresetInstalled(logger: Context['logger']): void { } catch (error) { logger.error('netxops: failed to sync declarative preset into profile patch: %s', error) } + try { + // Plugin Manager often leaves `- id: netxops` / `disabled: false` with no + // config; DSH 0.2 replaces the whole Config → capability/public flags never + // apply. Expand missing keys from package defaults on every Host apply. + healNetxopsProfileEntries(logger) + } catch (error) { + logger.error('netxops: failed to heal host profile entry: %s', error) + } } diff --git a/src/client/controller.ts b/src/client/controller.ts index 8ff3819..24678c3 100644 --- a/src/client/controller.ts +++ b/src/client/controller.ts @@ -188,7 +188,8 @@ export class NetxopsCardController { textField('nmsProvider'), textField('kbRoot'), booleanFieldPersistFalse('groupOpsInPreset'), - booleanField('groupOpsPublic'), + // Default is true — must persist false explicitly or "off" snaps back. + booleanFieldPersistFalse('groupOpsPublic'), booleanField('groupTopologyInPreset'), booleanField('groupTopologyPublic'), booleanFieldPersistFalse('groupBizMonitorInPreset'), diff --git a/src/client/index.ts b/src/client/index.ts index faa2230..c3b2c47 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -174,7 +174,7 @@ const MEMORY_DEFAULTS: NetxopsSettings = { imTargetId: '', imTargets: '', groupOpsInPreset: true, - groupOpsPublic: false, + groupOpsPublic: true, groupTopologyInPreset: false, groupTopologyPublic: false, groupBizMonitorInPreset: false, diff --git a/src/index.ts b/src/index.ts index 689754f..1100bd5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -145,7 +145,7 @@ export interface Config { nmsProvider: string /** ops tools/skills in the Netx Ops preset (default on) — NMS + CLI + paths. */ groupOpsInPreset: boolean - /** Publish ops tools/skills to other presets (default off). */ + /** Publish ops tools/skills to other presets (default on). */ groupOpsPublic: boolean /** topology canvas / layout tools in the Netx Ops preset (default off). */ groupTopologyInPreset: boolean @@ -193,7 +193,7 @@ export const Config: z = z.object({ imTargetId: vol(z.string().default('')), nmsProvider: vol(z.string().default('zte-ume')), groupOpsInPreset: vol(z.boolean().default(true)), - groupOpsPublic: vol(z.boolean().default(false)), + groupOpsPublic: vol(z.boolean().default(true)), groupTopologyInPreset: vol(z.boolean().default(false)), groupTopologyPublic: vol(z.boolean().default(false)), groupBizMonitorInPreset: vol(z.boolean().default(true)), diff --git a/src/netx/capability-groups.ts b/src/netx/capability-groups.ts index 2803621..21e7a40 100644 --- a/src/netx/capability-groups.ts +++ b/src/netx/capability-groups.ts @@ -36,7 +36,7 @@ export interface NetxCapabilityGroupSettingsFields { * Default: ops in Ops preset; topology / bizMonitor and all public off. */ export const DEFAULT_CAPABILITY_GROUPS: NetxCapabilityGroups = Object.freeze({ - ops: Object.freeze({ inPreset: true, public: false }), + ops: Object.freeze({ inPreset: true, public: true }), topology: Object.freeze({ inPreset: false, public: false }), bizMonitor: Object.freeze({ inPreset: true, public: false }), }) @@ -151,10 +151,17 @@ export function capabilityGroupsFromSettings( return { ops: { inPreset: opsInPreset, - public: src.groupOpsPublic === true - || src.groupNmsPublic === true - || src.groupCommonPublic === true - || src.groupManagedNePublic === true, + public: (() => { + if (src.groupOpsPublic !== undefined) return src.groupOpsPublic !== false + const legacy = [ + src.groupNmsPublic, + src.groupCommonPublic, + src.groupManagedNePublic, + ].filter((v): v is boolean => v !== undefined) + // No explicit flag → default on (other presets see ops after install). + if (legacy.length === 0) return true + return legacy.some((v) => v === true) + })(), }, topology: { inPreset: src.groupTopologyInPreset === true diff --git a/src/netx/heal-profile-entry.ts b/src/netx/heal-profile-entry.ts new file mode 100644 index 0000000..bc77f19 --- /dev/null +++ b/src/netx/heal-profile-entry.ts @@ -0,0 +1,203 @@ +/** + * Heal sparse Desktop/Web profile overrides for the host `netxops` row. + * + * Plugin Manager often writes only: + * - id: netxops + * disabled: false + * On DSH 0.2 that override **replaces the whole Config**, so capability / + * public flags never land unless we re-expand the entry. Call this on Host + * apply so every install (not just hand-edited machines) gets a usable row. + */ + +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { join } from 'node:path' +import type { Context } from '@deepseek-ai/cordis' + +const DESKTOP_PATCH_BEGIN = '# BEGIN dsh-netxops-preset (managed)' +const DESKTOP_PATCH_END = '# END dsh-netxops-preset (managed)' + +/** Defaults merged into missing keys only — never overwrite user values. */ +export const NETXOPS_HOST_CONFIG_DEFAULTS: Readonly> = Object.freeze({ + apiUrl: 'http://127.0.0.1:8890', + lang: 'zh', + thinkingLanguage: 'auto', + replyLanguage: 'follow-user', + nmsProvider: 'zte-ume', + tokenCredentialRef: 'NETX_API_TOKEN', + toolCallTimeoutMs: 120_000, + installAgentPreset: true, + alarmPushEnabled: false, + alarmDeliverDsh: true, + alarmDeliverIm: false, + imBotId: '', + imTargetId: '', + imTargets: '', + groupOpsInPreset: true, + /** Default on: other presets inherit ops tools/skills after install. */ + groupOpsPublic: true, + groupTopologyInPreset: false, + groupTopologyPublic: false, + groupBizMonitorInPreset: true, + groupBizMonitorPublic: false, + kbRoot: '', + groupKbInPreset: true, + groupKbPublic: false, +}) + +function resolveDshHome(): string { + const fromEnv = process.env.DSH_HOME?.trim() + if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv + return join(homedir(), '.dsh') +} + +function managedLineMask(lines: string[]): boolean[] { + const skip = lines.map(() => false) + let inside = false + for (let i = 0; i < lines.length; i += 1) { + const line = lines[i]! + if (line.includes(DESKTOP_PATCH_BEGIN)) inside = true + if (inside) skip[i] = true + if (line.includes(DESKTOP_PATCH_END)) inside = false + } + return skip +} + +function parseScalar(raw: string): string | boolean | number { + const text = raw.trim() + if (text === 'true') return true + if (text === 'false') return false + if (/^-?\d+$/.test(text)) return Number(text) + if ( + (text.startsWith('"') && text.endsWith('"')) + || (text.startsWith("'") && text.endsWith("'")) + ) { + return text.slice(1, -1) + } + return text +} + +function formatScalar(value: string | boolean | number): string { + if (typeof value === 'boolean' || typeof value === 'number') return String(value) + if (value === '') return "''" + if (/[:#\n\r]|^\s|\s$/.test(value) || value.includes("'")) { + return JSON.stringify(value) + } + return value +} + +interface HostEntry { + start: number + end: number + disabled?: boolean + name?: string + config: Record +} + +function findHostEntry(lines: string[], skip: boolean[]): HostEntry | null { + for (let i = 0; i < lines.length; i += 1) { + if (skip[i]) continue + if (lines[i] !== '- id: netxops') continue + let end = i + 1 + while (end < lines.length) { + const line = lines[end]! + if (skip[end]) break + if (/^- id:/.test(line)) break + end += 1 + } + while (end > i + 1 && lines[end - 1]!.trim() === '') end -= 1 + + let disabled: boolean | undefined + let name: string | undefined + const config: Record = {} + let inConfig = false + for (let j = i + 1; j < end; j += 1) { + const line = lines[j]! + const disabledMatch = /^ disabled:\s*(.+)\s*$/.exec(line) + if (disabledMatch) { + disabled = parseScalar(disabledMatch[1]!) === true + inConfig = false + continue + } + const nameMatch = /^ name:\s*(.+)\s*$/.exec(line) + if (nameMatch) { + name = String(parseScalar(nameMatch[1]!)) + inConfig = false + continue + } + if (/^ config:\s*$/.test(line)) { + inConfig = true + continue + } + if (inConfig) { + const cfg = /^ ([A-Za-z0-9_]+):\s*(.*?)\s*$/.exec(line) + if (cfg) { + config[cfg[1]!] = parseScalar(cfg[2]!) + continue + } + if (/^ \S/.test(line)) inConfig = false + } + } + return { start: i, end, disabled, name, config } + } + return null +} + +function needsHeal(entry: HostEntry): boolean { + if (entry.name !== 'dsh-netxops') return true + for (const key of Object.keys(NETXOPS_HOST_CONFIG_DEFAULTS)) { + if (!Object.hasOwn(entry.config, key)) return true + } + return false +} + +function renderHostEntry(entry: HostEntry): string[] { + const config = { ...NETXOPS_HOST_CONFIG_DEFAULTS, ...entry.config } + const out: string[] = [ + '- id: netxops', + " name: dsh-netxops", + ] + if (entry.disabled === true) out.push(' disabled: true') + else if (entry.disabled === false) out.push(' disabled: false') + out.push(' config:') + for (const [key, value] of Object.entries(config)) { + out.push(` ${key}: ${formatScalar(value)}`) + } + return out +} + +/** + * Expand sparse `netxops` host overrides in desktop/web profile patches. + * @returns number of profiles rewritten. + */ +export function healNetxopsProfileEntries(logger: Context['logger']): number { + let rewritten = 0 + for (const profileName of ['desktop', 'web'] as const) { + const patchPath = join(resolveDshHome(), 'profiles', profileName, 'cordis.patch.yml') + if (!existsSync(patchPath)) continue + const raw = readFileSync(patchPath, 'utf8') + const nl = raw.includes('\r\n') ? '\r\n' : '\n' + const lines = raw.replace(/\r\n/g, '\n').split('\n') + // Drop trailing empty split artifact for rewrite math; re-join with file nl. + const skip = managedLineMask(lines) + const entry = findHostEntry(lines, skip) + if (entry === null) continue + if (!needsHeal(entry)) continue + const rendered = renderHostEntry(entry) + const next = [ + ...lines.slice(0, entry.start), + ...rendered, + ...lines.slice(entry.end), + ].join(nl) + const normalized = next.endsWith(nl) ? next : `${next}${nl}` + if (normalized === raw) continue + writeFileSync(patchPath, normalized, 'utf8') + rewritten += 1 + logger.info( + 'netxops: healed sparse host entry in profiles/%s/cordis.patch.yml (restart %s to load capability defaults)', + profileName, + profileName === 'desktop' ? 'Desktop' : 'dsh web', + ) + } + return rewritten +}