mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-15 06:10:45 +08:00
Restore missing dsh-netxops host bridge after dirty reinstalls (0.1.53).
Preset-only self-heal left tools waiting forever when the bundle host row was dropped; insert the full host block into cordis.patch.yml and require one restart. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e2e65b0324
commit
777589b702
12 changed files with 1623 additions and 73 deletions
|
|
@ -1038,6 +1038,244 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/heal-profile-entry.ts
|
||||
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join as join5 } from "node:path";
|
||||
var DESKTOP_PATCH_BEGIN = "# BEGIN dsh-netxops-preset (managed)";
|
||||
var DESKTOP_PATCH_END = "# END dsh-netxops-preset (managed)";
|
||||
var HOST_HEAL_ONCE = Symbol.for("dsh-netxops.host-bridge-heal-attempted");
|
||||
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 join5(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 hostBridgeNamedElsewhere(lines, skip) {
|
||||
for (let i = 0;i < lines.length; i += 1) {
|
||||
if (skip[i])
|
||||
continue;
|
||||
if (/^\s+name:\s*['"]?dsh-netxops['"]?\s*$/.test(lines[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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
|
||||
out.push(" disabled: false");
|
||||
out.push(" config:");
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
out.push(` ${key}: ${formatScalar(value)}`);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function ensureNetxopsHostInProfilePatches(logger) {
|
||||
let rewritten = 0;
|
||||
let inserted = 0;
|
||||
for (const profileName of ["desktop", "web"]) {
|
||||
const profileDir = join5(resolveDshHome(), "profiles", profileName);
|
||||
const patchPath = join5(profileDir, "cordis.patch.yml");
|
||||
if (!existsSync4(profileDir))
|
||||
continue;
|
||||
const raw = existsSync4(patchPath) ? readFileSync2(patchPath, "utf8") : "";
|
||||
const nl = raw.includes(`\r
|
||||
`) ? `\r
|
||||
` : `
|
||||
`;
|
||||
const lines = raw.length > 0 ? raw.replace(/\r\n/g, `
|
||||
`).split(`
|
||||
`) : [];
|
||||
const skip = managedLineMask(lines);
|
||||
const entry = findHostEntry(lines, skip);
|
||||
let nextLines = null;
|
||||
let kind = null;
|
||||
if (entry === null) {
|
||||
if (hostBridgeNamedElsewhere(lines, skip))
|
||||
continue;
|
||||
const rendered = renderHostEntry({ disabled: false, config: {} });
|
||||
const beginIdx = lines.findIndex((line) => line.includes(DESKTOP_PATCH_BEGIN));
|
||||
if (beginIdx >= 0) {
|
||||
nextLines = [...lines.slice(0, beginIdx), ...rendered, "", ...lines.slice(beginIdx)];
|
||||
} else if (lines.length === 0 || lines.length === 1 && lines[0].trim() === "") {
|
||||
nextLines = [...rendered, ""];
|
||||
} else {
|
||||
const body = [...lines];
|
||||
while (body.length > 0 && body[body.length - 1].trim() === "")
|
||||
body.pop();
|
||||
nextLines = [...body, "", ...rendered, ""];
|
||||
}
|
||||
kind = "insert";
|
||||
} else if (needsHeal(entry)) {
|
||||
nextLines = [
|
||||
...lines.slice(0, entry.start),
|
||||
...renderHostEntry(entry),
|
||||
...lines.slice(entry.end)
|
||||
];
|
||||
kind = "rewrite";
|
||||
}
|
||||
if (nextLines === null || kind === null)
|
||||
continue;
|
||||
const normalized = nextLines.join(nl);
|
||||
const withNl = normalized.endsWith(nl) ? normalized : `${normalized}${nl}`;
|
||||
if (withNl === raw)
|
||||
continue;
|
||||
writeFileSync2(patchPath, withNl, "utf8");
|
||||
if (kind === "insert") {
|
||||
inserted += 1;
|
||||
logger.error("netxops: host bridge dsh-netxops was MISSING from profiles/%s/cordis.patch.yml — " + "wrote it back. Fully quit and restart %s once so the host publishes the connection " + "(preset-only installs leave netxops-tools waiting forever).", profileName, profileName === "desktop" ? "Desktop" : "dsh web");
|
||||
} else {
|
||||
rewritten += 1;
|
||||
logger.info("netxops: healed sparse host entry in profiles/%s/cordis.patch.yml " + "(public toggles hot-reload via settings; restart %s only if tools still missing)", profileName, profileName === "desktop" ? "Desktop" : "dsh web");
|
||||
}
|
||||
}
|
||||
return { rewritten, inserted };
|
||||
}
|
||||
function tryRecoverMissingHostBridge(logger, pluginName) {
|
||||
const root = globalThis;
|
||||
if (root[HOST_HEAL_ONCE] === true)
|
||||
return;
|
||||
root[HOST_HEAL_ONCE] = true;
|
||||
try {
|
||||
const result = ensureNetxopsHostInProfilePatches(logger);
|
||||
if (result.inserted === 0 && result.rewritten === 0) {
|
||||
logger.warn("%s: no connection yet — host settings bridge not publishing. " + "Check that profile cordis.yml contains name: dsh-netxops (not only preset-netxops). " + "Recovery: dsh plugin remove dsh-netxops && dsh plugin add …, then fully restart.", pluginName);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("%s: host-bridge recover failed: %s", pluginName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
|
|
@ -3026,7 +3264,7 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
unregisterTools = undefined;
|
||||
const connection = getNetxConnection();
|
||||
if (connection === undefined) {
|
||||
ctx.logger.warn("%s: no connection yet — waiting for host settings bridge", options.name);
|
||||
tryRecoverMissingHostBridge(ctx.logger, options.name);
|
||||
return;
|
||||
}
|
||||
const enabled = resolveGroups();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue