Add operator-subset knowledge base wiring for Netx Ops P1.

Locate and validate MANIFEST under kbRoot, show anti-mixup status in settings, and inject KB_* env plus kb-context so sessions degrade to pure netx when unset.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-13 10:39:28 +08:00
parent e6f9d062ed
commit de7c6a0c4b
23 changed files with 2694 additions and 68 deletions

View file

@ -244,6 +244,399 @@ function setImTargetSelected(current, target, selected) {
return normalized ? [...without, normalized] : without;
}
// src/netx/kb-manifest.ts
var import_node_fs = (() => ({}));
// node:path
function assertPath(path) {
if (typeof path !== "string")
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
}
function normalizeStringPosix(path, allowAboveRoot) {
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
for (var i = 0;i <= path.length; ++i) {
if (i < path.length)
code = path.charCodeAt(i);
else if (code === 47)
break;
else
code = 47;
if (code === 47) {
if (lastSlash === i - 1 || dots === 1)
;
else if (lastSlash !== i - 1 && dots === 2) {
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
if (res.length > 2) {
var lastSlashIndex = res.lastIndexOf("/");
if (lastSlashIndex !== res.length - 1) {
if (lastSlashIndex === -1)
res = "", lastSegmentLength = 0;
else
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
lastSlash = i, dots = 0;
continue;
}
} else if (res.length === 2 || res.length === 1) {
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
continue;
}
}
if (allowAboveRoot) {
if (res.length > 0)
res += "/..";
else
res = "..";
lastSegmentLength = 2;
}
} else {
if (res.length > 0)
res += "/" + path.slice(lastSlash + 1, i);
else
res = path.slice(lastSlash + 1, i);
lastSegmentLength = i - lastSlash - 1;
}
lastSlash = i, dots = 0;
} else if (code === 46 && dots !== -1)
++dots;
else
dots = -1;
}
return res;
}
function _format(sep, pathObject) {
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
if (!dir)
return base;
if (dir === pathObject.root)
return dir + base;
return dir + sep + base;
}
function resolve() {
var resolvedPath = "", resolvedAbsolute = false, cwd;
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
var path;
if (i >= 0)
path = arguments[i];
else {
if (cwd === undefined)
cwd = process.cwd();
path = cwd;
}
if (assertPath(path), path.length === 0)
continue;
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
}
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
if (resolvedPath.length > 0)
return "/" + resolvedPath;
else
return "/";
else if (resolvedPath.length > 0)
return resolvedPath;
else
return ".";
}
function normalize(path) {
if (assertPath(path), path.length === 0)
return ".";
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
path = ".";
if (path.length > 0 && trailingSeparator)
path += "/";
if (isAbsolute)
return "/" + path;
return path;
}
function isAbsolute(path) {
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
}
function join() {
if (arguments.length === 0)
return ".";
var joined;
for (var i = 0;i < arguments.length; ++i) {
var arg = arguments[i];
if (assertPath(arg), arg.length > 0)
if (joined === undefined)
joined = arg;
else
joined += "/" + arg;
}
if (joined === undefined)
return ".";
return normalize(joined);
}
function relative(from, to) {
if (assertPath(from), assertPath(to), from === to)
return "";
if (from = resolve(from), to = resolve(to), from === to)
return "";
var fromStart = 1;
for (;fromStart < from.length; ++fromStart)
if (from.charCodeAt(fromStart) !== 47)
break;
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
for (;toStart < to.length; ++toStart)
if (to.charCodeAt(toStart) !== 47)
break;
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
for (;i <= length; ++i) {
if (i === length) {
if (toLen > length) {
if (to.charCodeAt(toStart + i) === 47)
return to.slice(toStart + i + 1);
else if (i === 0)
return to.slice(toStart + i);
} else if (fromLen > length) {
if (from.charCodeAt(fromStart + i) === 47)
lastCommonSep = i;
else if (i === 0)
lastCommonSep = 0;
}
break;
}
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
if (fromCode !== toCode)
break;
else if (fromCode === 47)
lastCommonSep = i;
}
var out = "";
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
if (i === fromEnd || from.charCodeAt(i) === 47)
if (out.length === 0)
out += "..";
else
out += "/..";
if (out.length > 0)
return out + to.slice(toStart + lastCommonSep);
else {
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
++toStart;
return to.slice(toStart);
}
}
function _makeLong(path) {
return path;
}
function dirname(path) {
if (assertPath(path), path.length === 0)
return ".";
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
for (var i = path.length - 1;i >= 1; --i)
if (code = path.charCodeAt(i), code === 47) {
if (!matchedSlash) {
end = i;
break;
}
} else
matchedSlash = false;
if (end === -1)
return hasRoot ? "/" : ".";
if (hasRoot && end === 1)
return "//";
return path.slice(0, end);
}
function basename(path, ext) {
if (ext !== undefined && typeof ext !== "string")
throw TypeError('"ext" argument must be a string');
assertPath(path);
var start = 0, end = -1, matchedSlash = true, i;
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
if (ext.length === path.length && ext === path)
return "";
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
for (i = path.length - 1;i >= 0; --i) {
var code = path.charCodeAt(i);
if (code === 47) {
if (!matchedSlash) {
start = i + 1;
break;
}
} else {
if (firstNonSlashEnd === -1)
matchedSlash = false, firstNonSlashEnd = i + 1;
if (extIdx >= 0)
if (code === ext.charCodeAt(extIdx)) {
if (--extIdx === -1)
end = i;
} else
extIdx = -1, end = firstNonSlashEnd;
}
}
if (start === end)
end = firstNonSlashEnd;
else if (end === -1)
end = path.length;
return path.slice(start, end);
} else {
for (i = path.length - 1;i >= 0; --i)
if (path.charCodeAt(i) === 47) {
if (!matchedSlash) {
start = i + 1;
break;
}
} else if (end === -1)
matchedSlash = false, end = i + 1;
if (end === -1)
return "";
return path.slice(start, end);
}
}
function extname(path) {
assertPath(path);
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
for (var i = path.length - 1;i >= 0; --i) {
var code = path.charCodeAt(i);
if (code === 47) {
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1)
matchedSlash = false, end = i + 1;
if (code === 46) {
if (startDot === -1)
startDot = i;
else if (preDotState !== 1)
preDotState = 1;
} else if (startDot !== -1)
preDotState = -1;
}
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
return "";
return path.slice(startDot, end);
}
function format(pathObject) {
if (pathObject === null || typeof pathObject !== "object")
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
return _format("/", pathObject);
}
function parse(path) {
assertPath(path);
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
if (path.length === 0)
return ret;
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
if (isAbsolute2)
ret.root = "/", start = 1;
else
start = 0;
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
for (;i >= start; --i) {
if (code = path.charCodeAt(i), code === 47) {
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1)
matchedSlash = false, end = i + 1;
if (code === 46) {
if (startDot === -1)
startDot = i;
else if (preDotState !== 1)
preDotState = 1;
} else if (startDot !== -1)
preDotState = -1;
}
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
if (end !== -1)
if (startPart === 0 && isAbsolute2)
ret.base = ret.name = path.slice(1, end);
else
ret.base = ret.name = path.slice(startPart, end);
} else {
if (startPart === 0 && isAbsolute2)
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
else
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
ret.ext = path.slice(startDot, end);
}
if (startPart > 0)
ret.dir = path.slice(0, startPart - 1);
else if (isAbsolute2)
ret.dir = "/";
return ret;
}
var sep = "/";
var delimiter = ":";
var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
// src/netx/kb-manifest.ts
var EMPTY_CONTENT = {
regions: false,
theory: false,
packet: false,
skills: false
};
function unconfiguredKbSnapshot() {
return {
status: "unconfigured",
realRoot: "",
operatorName: "",
country: "",
version: "",
content: { ...EMPTY_CONTENT },
errorMessage: ""
};
}
// src/client/kb-status-view.ts
var NETXOPS_RPC_CHANNEL2 = "/netxops";
var KB_STATUS_ENDPOINT = "kb.status";
var KB_RESOLVE_ENDPOINT = "kb.resolve";
function asKbSnapshot(value) {
const empty = unconfiguredKbSnapshot();
if (value === null || typeof value !== "object" || Array.isArray(value))
return empty;
const row = value;
const status = row.status === "configured" || row.status === "error" || row.status === "unconfigured" ? row.status : "unconfigured";
const contentRaw = row.content;
const content = { ...empty.content };
if (contentRaw !== null && typeof contentRaw === "object" && !Array.isArray(contentRaw)) {
for (const [key, flag] of Object.entries(contentRaw)) {
content[key] = flag === true;
}
}
return {
status,
realRoot: typeof row.realRoot === "string" ? row.realRoot : "",
operatorName: typeof row.operatorName === "string" ? row.operatorName : "",
country: typeof row.country === "string" ? row.country : "",
version: typeof row.version === "string" ? row.version : "",
content,
errorMessage: typeof row.errorMessage === "string" ? row.errorMessage : ""
};
}
async function fetchKbStatus(call, signal) {
const result = await call(NETXOPS_RPC_CHANNEL2, KB_STATUS_ENDPOINT, {}, signal);
if (result !== null && typeof result === "object" && result.ok === true) {
return asKbSnapshot(result.value);
}
return unconfiguredKbSnapshot();
}
async function resolveKbPath(call, path, signal) {
const result = await call(NETXOPS_RPC_CHANNEL2, KB_RESOLVE_ENDPOINT, { path }, signal);
if (result !== null && typeof result === "object" && result.ok === true) {
return asKbSnapshot(result.value);
}
return unconfiguredKbSnapshot();
}
function kbStatusTone(status) {
if (status === "configured")
return "ok";
if (status === "error")
return "err";
if (status === "unconfigured")
return "warn";
return "neutral";
}
// src/client/styles.ts
var STYLE_ID = "dsh-netxops-settings-css";
var CSS = `
@ -285,7 +678,9 @@ var CSS = `
.dsh-nx-groupTitle{font-size:13px;font-weight:600;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2329)}
.dsh-nx-groupChecks{display:flex;flex-direction:column;gap:8px;padding-left:2px}
.dsh-nx-imTargetList{display:flex;flex-direction:column;gap:8px;padding:4px 0 2px}
.dsh-nx-settings-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:4px}
.dsh-nx-pathRow{display:flex;gap:8px;align-items:stretch}
.dsh-nx-pathRow>input{flex:1;min-width:0}
.dsh-nx-pathRow>.dsh-nx-btn{flex:none;align-self:stretch}
.dsh-nx-settings-msg{font-size:12px;color:var(--dsw-alias-label-secondary,#646a73)}
.dsh-nx-settings-msg.ok{color:var(--dsw-alias-state-success-primary,#20a162)}
.dsh-nx-settings-msg.err{color:var(--dsw-alias-state-error-primary,#d54941)}
@ -466,13 +861,26 @@ function phaseLocaleKey(phase) {
}
}
function StatusBadge(props) {
const tone = alarmPushTone(props.phase);
const tone = props.tone ?? (props.phase !== undefined ? alarmPushTone(props.phase) : "neutral");
const className = tone === "ok" ? "dsh-nx-status dsh-nx-statusOk" : tone === "warn" ? "dsh-nx-status dsh-nx-statusWarn" : tone === "err" ? "dsh-nx-status dsh-nx-statusErr" : "dsh-nx-status";
return /* @__PURE__ */ jsx_runtime.jsx("span", {
className,
children: props.label
});
}
function kbBadgeLabel(t, snapshot) {
if (!snapshot || snapshot.status === "unconfigured") {
return t("kbStatusUnconfigured");
}
if (snapshot.status === "error") {
return fillTemplate(t("kbStatusError"), { detail: snapshot.errorMessage || "error" });
}
return fillTemplate(t("kbStatusConfigured"), {
operator: snapshot.operatorName,
country: snapshot.country,
version: snapshot.version
});
}
function fillTemplate(template, vars) {
return template.replace(/\{(\w+)\}/g, (match, name) => Object.prototype.hasOwnProperty.call(vars, name) ? vars[name] : match);
}
@ -907,6 +1315,92 @@ function NetxopsCard(props) {
})
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-settings-card",
children: [
/* @__PURE__ */ jsx_runtime.jsx("h3", {
children: t("sectionKnowledge")
}),
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-settings-field",
children: [
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-field-head",
children: [
/* @__PURE__ */ jsx_runtime.jsx("label", {
htmlFor: "netxops-kb-root",
children: t("kbRoot")
}),
/* @__PURE__ */ jsx_runtime.jsxs("span", {
className: "dsh-nx-badges",
children: [
/* @__PURE__ */ jsx_runtime.jsx(StatusBadge, {
tone: kbStatusTone(state.kbStatus?.status ?? "unconfigured"),
label: kbBadgeLabel(t, state.kbStatus)
}),
state.kbRoot.overridden ? /* @__PURE__ */ jsx_runtime.jsxs(jsx_runtime.Fragment, {
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("kbRoot");
},
children: t("reset")
})
]
}) : null
]
})
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-pathRow",
children: [
/* @__PURE__ */ jsx_runtime.jsx("input", {
id: "netxops-kb-root",
className: state.kbRoot.invalid ? "dsh-nx-inputInvalid" : undefined,
type: "text",
value: state.kbRoot.text,
disabled,
"aria-invalid": state.kbRoot.invalid || undefined,
onChange: (event) => {
props.edit("kbRoot", event.target.value);
}
}),
/* @__PURE__ */ jsx_runtime.jsx("button", {
type: "button",
className: "dsh-nx-btn",
disabled: disabled || !state.kbDirectoryPickerReady,
onClick: () => {
props.browseKbRoot();
},
children: t("kbBrowse")
})
]
}),
/* @__PURE__ */ jsx_runtime.jsx("p", {
className: state.kbRoot.invalid ? "dsh-nx-invalid" : "dsh-nx-hint",
children: state.kbRoot.invalid ? t("invalid") : state.kbDirectoryPickerReady ? t("kbRootHint") : t("kbBrowseUnavailable")
}),
state.kbStatus?.status === "error" && state.kbStatus.errorMessage ? /* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-invalid",
role: "status",
children: state.kbStatus.errorMessage
}) : null,
state.kbStatus?.status === "configured" && state.kbStatus.realRoot ? /* @__PURE__ */ jsx_runtime.jsx("p", {
className: "dsh-nx-hint",
children: state.kbStatus.realRoot
}) : null
]
})
]
}),
/* @__PURE__ */ jsx_runtime.jsxs("div", {
className: "dsh-nx-settings-card",
children: [
@ -1307,6 +1801,11 @@ class NetxopsCardController {
};
rpcCall;
alarmPushStatus = null;
kbStatus = null;
kbPreview = null;
kbDirectoryPicker;
kbBrowseInFlight = false;
kbStatusInFlight = false;
imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG };
sessionsExportStatus = null;
sessionsExportBusy = false;
@ -1324,6 +1823,7 @@ class NetxopsCardController {
textField("apiUrl"),
textField("lang"),
textField("nmsProvider"),
textField("kbRoot"),
booleanFieldPersistFalse("groupOpsInPreset"),
booleanField("groupOpsPublic"),
booleanField("groupTopologyInPreset"),
@ -1338,6 +1838,7 @@ class NetxopsCardController {
this.store = this.form.bind(() => this.projection());
scope.subscribe(() => {
this.readCredential();
this.refreshKbPreview();
});
this.readCredential();
}
@ -1362,6 +1863,11 @@ class NetxopsCardController {
this.alarmPushStatus = null;
changed = true;
}
if (this.kbStatus !== null || this.kbPreview !== null) {
this.kbStatus = null;
this.kbPreview = null;
changed = true;
}
if (this.imDeliveryCatalog.options.length > 0 || this.imDeliveryCatalog.available !== true) {
this.imDeliveryCatalog = { ...EMPTY_IM_DELIVERY_CATALOG };
changed = true;
@ -1376,14 +1882,25 @@ class NetxopsCardController {
}
this.startStatusPoll();
this.refreshAlarmPushStatus();
this.refreshKbStatus();
this.refreshKbPreview();
this.refreshImDeliveryCatalog();
this.refreshSessionsExportStatus();
}
setDirectoryPicker(picker) {
const ready = picker !== undefined;
if (this.kbDirectoryPicker === picker && this.kbDirectoryPicker !== undefined === ready) {
return;
}
this.kbDirectoryPicker = picker;
this.store.set(this.projection());
}
startStatusPoll() {
if (this.pollTimer !== undefined)
return;
this.pollTimer = setInterval(() => {
this.refreshAlarmPushStatus();
this.refreshKbStatus();
this.refreshImDeliveryCatalog();
this.refreshSessionsExportStatus();
}, STATUS_POLL_MS);
@ -1446,6 +1963,58 @@ class NetxopsCardController {
this.exportStatusInFlight = false;
}
}
async refreshKbStatus() {
const call = this.rpcCall;
if (call === undefined || this.kbStatusInFlight)
return;
this.kbStatusInFlight = true;
try {
const next = await fetchKbStatus(call);
const prev = this.kbStatus;
if (prev && prev.status === next.status && prev.realRoot === next.realRoot && prev.operatorName === next.operatorName && prev.country === next.country && prev.version === next.version && prev.errorMessage === next.errorMessage)
return;
this.kbStatus = next;
this.store.set(this.projection());
} catch {} finally {
this.kbStatusInFlight = false;
}
}
async refreshKbPreview() {
const call = this.rpcCall;
if (call === undefined)
return;
const draft = this.form.field("kbRoot").text.trim();
const saved = (this.scope.getSnapshot().value?.kbRoot ?? "").trim();
if (draft === saved) {
if (this.kbPreview !== null) {
this.kbPreview = null;
this.store.set(this.projection());
}
return;
}
try {
const next = draft === "" ? unconfiguredKbSnapshot() : await resolveKbPath(call, draft);
const prev = this.kbPreview;
if (prev && prev.status === next.status && prev.realRoot === next.realRoot && prev.operatorName === next.operatorName && prev.country === next.country && prev.version === next.version && prev.errorMessage === next.errorMessage)
return;
this.kbPreview = next;
this.store.set(this.projection());
} catch {}
}
browseKbRoot() {
const picker = this.kbDirectoryPicker;
if (picker === undefined || this.kbBrowseInFlight)
return;
this.kbBrowseInFlight = true;
picker.pick().then((path) => {
if (typeof path === "string" && path.trim() !== "") {
this.form.actions().edit("kbRoot", path);
this.refreshKbPreview();
}
}).catch(() => {}).finally(() => {
this.kbBrowseInFlight = false;
});
}
exportAllSessions() {
if (this.exportInFlight !== undefined || this.sessionsExportBusy)
return;
@ -1485,6 +2054,7 @@ class NetxopsCardController {
apiUrl: this.form.field("apiUrl"),
lang: this.form.field("lang"),
nmsProvider: this.form.field("nmsProvider"),
kbRoot: this.form.field("kbRoot"),
groupOpsInPreset: this.form.field("groupOpsInPreset"),
groupOpsPublic: this.form.field("groupOpsPublic"),
groupTopologyInPreset: this.form.field("groupTopologyInPreset"),
@ -1500,6 +2070,8 @@ class NetxopsCardController {
apiTokenWritable: this.credential.remoteReady && this.credential.writable,
apiTokenRemoteReady: this.credential.remoteReady,
alarmPushStatus: this.alarmPushStatus,
kbStatus: this.kbPreview ?? this.kbStatus,
kbDirectoryPickerReady: this.kbDirectoryPicker !== undefined,
imDeliveryCatalog: this.imDeliveryCatalog,
sessionsExportStatus: this.sessionsExportStatus,
sessionsExportBusy: this.sessionsExportBusy,
@ -1550,11 +2122,29 @@ class NetxopsCardController {
this.readCredential();
}
inject() {
const actions = this.form.actions();
return {
hooks: { netxopsCard: this.store },
...this.form.actions(),
...actions,
edit: (field, text) => {
actions.edit(field, text);
if (field === "kbRoot")
this.refreshKbPreview();
},
discard: () => {
actions.discard();
this.refreshKbPreview();
},
resetField: (field) => {
actions.resetField(field);
if (field === "kbRoot")
this.refreshKbPreview();
},
exportAllSessions: () => {
this.exportAllSessions();
},
browseKbRoot: () => {
this.browseKbRoot();
}
};
}
@ -1575,9 +2165,10 @@ function refOf(snapshot) {
// src/client/locales.ts
var en = {
title: "Netx Ops",
description: "API, token, capabilities, and alarm delivery.",
description: "API, token, capabilities, knowledge base, and alarm delivery.",
sectionConnection: "Connection",
sectionCapabilities: "Capability groups",
sectionKnowledge: "Knowledge base",
sectionAlarms: "Key-alarm delivery",
sectionExport: "Session export",
apiUrl: "API URL",
@ -1591,6 +2182,13 @@ var en = {
groupTopology: "topology",
groupInPreset: "In Netx Ops preset",
groupPublic: "Publish to other presets",
kbRoot: "Package root",
kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Empty = pure netx.",
kbBrowse: "Browse…",
kbBrowseUnavailable: "Directory picker unavailable — paste an absolute path.",
kbStatusConfigured: "Knowledge base: {operator} ({country}) v{version}",
kbStatusUnconfigured: "Knowledge base: not configured (pure netx)",
kbStatusError: "Knowledge base: error — {detail}",
alarmPushEnabled: "Key-alarm push",
alarmPushStatus: "Status",
alarmPushPhaseDisabled: "Off",
@ -1637,9 +2235,10 @@ var en = {
};
var zh = {
title: "Netx Ops",
description: "API、Token、能力组与告警投递。",
description: "API、Token、能力组、知识库与告警投递。",
sectionConnection: "连接",
sectionCapabilities: "能力组",
sectionKnowledge: "知识库",
sectionAlarms: "关键告警投递",
sectionExport: "会话导出",
apiUrl: "API 地址",
@ -1653,6 +2252,13 @@ var zh = {
groupTopology: "topology",
groupInPreset: "在 Netx Ops 预设中启用",
groupPublic: "对其他预设公开",
kbRoot: "知识包根目录",
kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。留空=纯 netx。",
kbBrowse: "浏览…",
kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对路径。",
kbStatusConfigured: "知识库: {operator}({country}) v{version}",
kbStatusUnconfigured: "知识库: 未配置(纯 netx)",
kbStatusError: "知识库: 错误 — {detail}",
alarmPushEnabled: "关键告警推送",
alarmPushStatus: "状态",
alarmPushPhaseDisabled: "未开启",
@ -1744,6 +2350,17 @@ function apply(ctx) {
card.setAlarmPushRpc(undefined);
}, "netxops: clear alarm-push rpc");
});
ctx.inject(["remote.directoryPicker"], (dpCtx) => {
const picker = dpCtx.remote?.directoryPicker;
if (!picker || typeof picker.pick !== "function") {
dpCtx.logger.warn("netxops: remote.directoryPicker.pick unavailable — browse button disabled");
return;
}
card.setDirectoryPicker(picker);
dpCtx.effect(() => () => {
card.setDirectoryPicker(undefined);
}, "netxops: clear directory picker");
});
ctx.slots.inject("settings.section", () => ctx.slots.register({
name: "settings.section",
id: NETXOPS_NS,