mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 13:30:48 +08:00
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:
parent
e6f9d062ed
commit
de7c6a0c4b
23 changed files with 2694 additions and 68 deletions
|
|
@ -215,22 +215,120 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.connection-store");
|
||||
// src/netx/kb-context-skill.ts
|
||||
var SKILL_NAME = "kb-context";
|
||||
function skillBody(snapshot) {
|
||||
if (snapshot.status === "configured") {
|
||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||
return {
|
||||
description: "Operator knowledge-base context for this Host (paths + identity from MANIFEST).",
|
||||
content: [
|
||||
"## Knowledge base (configured)",
|
||||
"",
|
||||
"When troubleshooting with operator playbooks or docs, **compose paths from this root**.",
|
||||
"Do not invent another operator or country.",
|
||||
"",
|
||||
`| Field | Value |`,
|
||||
`| --- | --- |`,
|
||||
`| kbStatus | configured |`,
|
||||
`| kbRoot | \`${snapshot.realRoot}\` |`,
|
||||
`| kbOperator | ${snapshot.operatorName} |`,
|
||||
`| kbCountry | ${snapshot.country} |`,
|
||||
`| kbVersion | ${snapshot.version} |`,
|
||||
`| kbContent (on) | ${flags} |`,
|
||||
"",
|
||||
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
const reason = snapshot.status === "error" ? snapshot.errorMessage || "invalid knowledge base" : "kbRoot is empty";
|
||||
return {
|
||||
description: "Knowledge-base context is unavailable — stay on pure netx evidence.",
|
||||
content: [
|
||||
"## Knowledge base (unavailable)",
|
||||
"",
|
||||
`kbStatus=\`${snapshot.status}\`. ${reason}`,
|
||||
"",
|
||||
"**Do not invent an operator, country, or KB paths.**",
|
||||
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
|
||||
"Operator-specific playbooks are out of scope until a valid MANIFEST is configured."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
function registerKbContextSkill(ctx, snapshot) {
|
||||
const skillsApi = ctx.skills;
|
||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const body = skillBody(snapshot);
|
||||
return skillsApi.register({
|
||||
name: SKILL_NAME,
|
||||
description: body.description,
|
||||
content: body.content,
|
||||
provider: "netxops-kb",
|
||||
source: "custom"
|
||||
});
|
||||
}
|
||||
|
||||
// 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/netx/kb-runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||
function store() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store().connection;
|
||||
return store2().connection;
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store();
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1749,6 +1847,7 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
remountTools();
|
||||
});
|
||||
ctx.inject(["skills"], (skillsCtx) => {
|
||||
let unregisterKbSkill;
|
||||
const remountSkills = () => {
|
||||
const gen = ++skillGeneration;
|
||||
unregisterSkills?.();
|
||||
|
|
@ -1766,15 +1865,27 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
skillsCtx.logger.warn("%s: skill register failed: %s", options.name, error);
|
||||
});
|
||||
};
|
||||
const remountKbSkill = () => {
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext());
|
||||
};
|
||||
remountSkills();
|
||||
remountKbSkill();
|
||||
const stopSkillWatch = watchNetxConnection(() => {
|
||||
remountSkills();
|
||||
});
|
||||
const stopKbWatch = watchKbContext(() => {
|
||||
remountKbSkill();
|
||||
});
|
||||
skillsCtx.effect(() => () => {
|
||||
skillGeneration += 1;
|
||||
stopSkillWatch();
|
||||
stopKbWatch();
|
||||
unregisterSkills?.();
|
||||
unregisterSkills = undefined;
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
}, `${options.name}: dispose skills`);
|
||||
});
|
||||
ctx.effect(() => () => {
|
||||
|
|
|
|||
|
|
@ -215,22 +215,120 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.connection-store");
|
||||
// src/netx/kb-context-skill.ts
|
||||
var SKILL_NAME = "kb-context";
|
||||
function skillBody(snapshot) {
|
||||
if (snapshot.status === "configured") {
|
||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||
return {
|
||||
description: "Operator knowledge-base context for this Host (paths + identity from MANIFEST).",
|
||||
content: [
|
||||
"## Knowledge base (configured)",
|
||||
"",
|
||||
"When troubleshooting with operator playbooks or docs, **compose paths from this root**.",
|
||||
"Do not invent another operator or country.",
|
||||
"",
|
||||
`| Field | Value |`,
|
||||
`| --- | --- |`,
|
||||
`| kbStatus | configured |`,
|
||||
`| kbRoot | \`${snapshot.realRoot}\` |`,
|
||||
`| kbOperator | ${snapshot.operatorName} |`,
|
||||
`| kbCountry | ${snapshot.country} |`,
|
||||
`| kbVersion | ${snapshot.version} |`,
|
||||
`| kbContent (on) | ${flags} |`,
|
||||
"",
|
||||
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
const reason = snapshot.status === "error" ? snapshot.errorMessage || "invalid knowledge base" : "kbRoot is empty";
|
||||
return {
|
||||
description: "Knowledge-base context is unavailable — stay on pure netx evidence.",
|
||||
content: [
|
||||
"## Knowledge base (unavailable)",
|
||||
"",
|
||||
`kbStatus=\`${snapshot.status}\`. ${reason}`,
|
||||
"",
|
||||
"**Do not invent an operator, country, or KB paths.**",
|
||||
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
|
||||
"Operator-specific playbooks are out of scope until a valid MANIFEST is configured."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
function registerKbContextSkill(ctx, snapshot) {
|
||||
const skillsApi = ctx.skills;
|
||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const body = skillBody(snapshot);
|
||||
return skillsApi.register({
|
||||
name: SKILL_NAME,
|
||||
description: body.description,
|
||||
content: body.content,
|
||||
provider: "netxops-kb",
|
||||
source: "custom"
|
||||
});
|
||||
}
|
||||
|
||||
// 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/netx/kb-runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||
function store() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store().connection;
|
||||
return store2().connection;
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store();
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1749,6 +1847,7 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
remountTools();
|
||||
});
|
||||
ctx.inject(["skills"], (skillsCtx) => {
|
||||
let unregisterKbSkill;
|
||||
const remountSkills = () => {
|
||||
const gen = ++skillGeneration;
|
||||
unregisterSkills?.();
|
||||
|
|
@ -1766,15 +1865,27 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
skillsCtx.logger.warn("%s: skill register failed: %s", options.name, error);
|
||||
});
|
||||
};
|
||||
const remountKbSkill = () => {
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext());
|
||||
};
|
||||
remountSkills();
|
||||
remountKbSkill();
|
||||
const stopSkillWatch = watchNetxConnection(() => {
|
||||
remountSkills();
|
||||
});
|
||||
const stopKbWatch = watchKbContext(() => {
|
||||
remountKbSkill();
|
||||
});
|
||||
skillsCtx.effect(() => () => {
|
||||
skillGeneration += 1;
|
||||
stopSkillWatch();
|
||||
stopKbWatch();
|
||||
unregisterSkills?.();
|
||||
unregisterSkills = undefined;
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
}, `${options.name}: dispose skills`);
|
||||
});
|
||||
ctx.effect(() => () => {
|
||||
|
|
|
|||
|
|
@ -215,22 +215,120 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.connection-store");
|
||||
// src/netx/kb-context-skill.ts
|
||||
var SKILL_NAME = "kb-context";
|
||||
function skillBody(snapshot) {
|
||||
if (snapshot.status === "configured") {
|
||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||
return {
|
||||
description: "Operator knowledge-base context for this Host (paths + identity from MANIFEST).",
|
||||
content: [
|
||||
"## Knowledge base (configured)",
|
||||
"",
|
||||
"When troubleshooting with operator playbooks or docs, **compose paths from this root**.",
|
||||
"Do not invent another operator or country.",
|
||||
"",
|
||||
`| Field | Value |`,
|
||||
`| --- | --- |`,
|
||||
`| kbStatus | configured |`,
|
||||
`| kbRoot | \`${snapshot.realRoot}\` |`,
|
||||
`| kbOperator | ${snapshot.operatorName} |`,
|
||||
`| kbCountry | ${snapshot.country} |`,
|
||||
`| kbVersion | ${snapshot.version} |`,
|
||||
`| kbContent (on) | ${flags} |`,
|
||||
"",
|
||||
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
const reason = snapshot.status === "error" ? snapshot.errorMessage || "invalid knowledge base" : "kbRoot is empty";
|
||||
return {
|
||||
description: "Knowledge-base context is unavailable — stay on pure netx evidence.",
|
||||
content: [
|
||||
"## Knowledge base (unavailable)",
|
||||
"",
|
||||
`kbStatus=\`${snapshot.status}\`. ${reason}`,
|
||||
"",
|
||||
"**Do not invent an operator, country, or KB paths.**",
|
||||
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
|
||||
"Operator-specific playbooks are out of scope until a valid MANIFEST is configured."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
function registerKbContextSkill(ctx, snapshot) {
|
||||
const skillsApi = ctx.skills;
|
||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const body = skillBody(snapshot);
|
||||
return skillsApi.register({
|
||||
name: SKILL_NAME,
|
||||
description: body.description,
|
||||
content: body.content,
|
||||
provider: "netxops-kb",
|
||||
source: "custom"
|
||||
});
|
||||
}
|
||||
|
||||
// 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/netx/kb-runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||
function store() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store().connection;
|
||||
return store2().connection;
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store();
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1749,6 +1847,7 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
remountTools();
|
||||
});
|
||||
ctx.inject(["skills"], (skillsCtx) => {
|
||||
let unregisterKbSkill;
|
||||
const remountSkills = () => {
|
||||
const gen = ++skillGeneration;
|
||||
unregisterSkills?.();
|
||||
|
|
@ -1766,15 +1865,27 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
skillsCtx.logger.warn("%s: skill register failed: %s", options.name, error);
|
||||
});
|
||||
};
|
||||
const remountKbSkill = () => {
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext());
|
||||
};
|
||||
remountSkills();
|
||||
remountKbSkill();
|
||||
const stopSkillWatch = watchNetxConnection(() => {
|
||||
remountSkills();
|
||||
});
|
||||
const stopKbWatch = watchKbContext(() => {
|
||||
remountKbSkill();
|
||||
});
|
||||
skillsCtx.effect(() => () => {
|
||||
skillGeneration += 1;
|
||||
stopSkillWatch();
|
||||
stopKbWatch();
|
||||
unregisterSkills?.();
|
||||
unregisterSkills = undefined;
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
}, `${options.name}: dispose skills`);
|
||||
});
|
||||
ctx.effect(() => () => {
|
||||
|
|
|
|||
|
|
@ -215,22 +215,120 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.connection-store");
|
||||
// src/netx/kb-context-skill.ts
|
||||
var SKILL_NAME = "kb-context";
|
||||
function skillBody(snapshot) {
|
||||
if (snapshot.status === "configured") {
|
||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||
return {
|
||||
description: "Operator knowledge-base context for this Host (paths + identity from MANIFEST).",
|
||||
content: [
|
||||
"## Knowledge base (configured)",
|
||||
"",
|
||||
"When troubleshooting with operator playbooks or docs, **compose paths from this root**.",
|
||||
"Do not invent another operator or country.",
|
||||
"",
|
||||
`| Field | Value |`,
|
||||
`| --- | --- |`,
|
||||
`| kbStatus | configured |`,
|
||||
`| kbRoot | \`${snapshot.realRoot}\` |`,
|
||||
`| kbOperator | ${snapshot.operatorName} |`,
|
||||
`| kbCountry | ${snapshot.country} |`,
|
||||
`| kbVersion | ${snapshot.version} |`,
|
||||
`| kbContent (on) | ${flags} |`,
|
||||
"",
|
||||
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
const reason = snapshot.status === "error" ? snapshot.errorMessage || "invalid knowledge base" : "kbRoot is empty";
|
||||
return {
|
||||
description: "Knowledge-base context is unavailable — stay on pure netx evidence.",
|
||||
content: [
|
||||
"## Knowledge base (unavailable)",
|
||||
"",
|
||||
`kbStatus=\`${snapshot.status}\`. ${reason}`,
|
||||
"",
|
||||
"**Do not invent an operator, country, or KB paths.**",
|
||||
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
|
||||
"Operator-specific playbooks are out of scope until a valid MANIFEST is configured."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
function registerKbContextSkill(ctx, snapshot) {
|
||||
const skillsApi = ctx.skills;
|
||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const body = skillBody(snapshot);
|
||||
return skillsApi.register({
|
||||
name: SKILL_NAME,
|
||||
description: body.description,
|
||||
content: body.content,
|
||||
provider: "netxops-kb",
|
||||
source: "custom"
|
||||
});
|
||||
}
|
||||
|
||||
// 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/netx/kb-runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||
function store() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store().connection;
|
||||
return store2().connection;
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store();
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1749,6 +1847,7 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
remountTools();
|
||||
});
|
||||
ctx.inject(["skills"], (skillsCtx) => {
|
||||
let unregisterKbSkill;
|
||||
const remountSkills = () => {
|
||||
const gen = ++skillGeneration;
|
||||
unregisterSkills?.();
|
||||
|
|
@ -1766,15 +1865,27 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
skillsCtx.logger.warn("%s: skill register failed: %s", options.name, error);
|
||||
});
|
||||
};
|
||||
const remountKbSkill = () => {
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext());
|
||||
};
|
||||
remountSkills();
|
||||
remountKbSkill();
|
||||
const stopSkillWatch = watchNetxConnection(() => {
|
||||
remountSkills();
|
||||
});
|
||||
const stopKbWatch = watchKbContext(() => {
|
||||
remountKbSkill();
|
||||
});
|
||||
skillsCtx.effect(() => () => {
|
||||
skillGeneration += 1;
|
||||
stopSkillWatch();
|
||||
stopKbWatch();
|
||||
unregisterSkills?.();
|
||||
unregisterSkills = undefined;
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
}, `${options.name}: dispose skills`);
|
||||
});
|
||||
ctx.effect(() => () => {
|
||||
|
|
|
|||
|
|
@ -215,22 +215,120 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.connection-store");
|
||||
// src/netx/kb-context-skill.ts
|
||||
var SKILL_NAME = "kb-context";
|
||||
function skillBody(snapshot) {
|
||||
if (snapshot.status === "configured") {
|
||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||
return {
|
||||
description: "Operator knowledge-base context for this Host (paths + identity from MANIFEST).",
|
||||
content: [
|
||||
"## Knowledge base (configured)",
|
||||
"",
|
||||
"When troubleshooting with operator playbooks or docs, **compose paths from this root**.",
|
||||
"Do not invent another operator or country.",
|
||||
"",
|
||||
`| Field | Value |`,
|
||||
`| --- | --- |`,
|
||||
`| kbStatus | configured |`,
|
||||
`| kbRoot | \`${snapshot.realRoot}\` |`,
|
||||
`| kbOperator | ${snapshot.operatorName} |`,
|
||||
`| kbCountry | ${snapshot.country} |`,
|
||||
`| kbVersion | ${snapshot.version} |`,
|
||||
`| kbContent (on) | ${flags} |`,
|
||||
"",
|
||||
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
const reason = snapshot.status === "error" ? snapshot.errorMessage || "invalid knowledge base" : "kbRoot is empty";
|
||||
return {
|
||||
description: "Knowledge-base context is unavailable — stay on pure netx evidence.",
|
||||
content: [
|
||||
"## Knowledge base (unavailable)",
|
||||
"",
|
||||
`kbStatus=\`${snapshot.status}\`. ${reason}`,
|
||||
"",
|
||||
"**Do not invent an operator, country, or KB paths.**",
|
||||
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
|
||||
"Operator-specific playbooks are out of scope until a valid MANIFEST is configured."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
function registerKbContextSkill(ctx, snapshot) {
|
||||
const skillsApi = ctx.skills;
|
||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const body = skillBody(snapshot);
|
||||
return skillsApi.register({
|
||||
name: SKILL_NAME,
|
||||
description: body.description,
|
||||
content: body.content,
|
||||
provider: "netxops-kb",
|
||||
source: "custom"
|
||||
});
|
||||
}
|
||||
|
||||
// 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/netx/kb-runtime.ts
|
||||
var STORE_KEY = Symbol.for("dsh-netxops.kb-store");
|
||||
function store() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getKbContext() {
|
||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store().connection;
|
||||
return store2().connection;
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store();
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -1749,6 +1847,7 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
remountTools();
|
||||
});
|
||||
ctx.inject(["skills"], (skillsCtx) => {
|
||||
let unregisterKbSkill;
|
||||
const remountSkills = () => {
|
||||
const gen = ++skillGeneration;
|
||||
unregisterSkills?.();
|
||||
|
|
@ -1766,15 +1865,27 @@ function applyGroupToolsPlugin(ctx, options) {
|
|||
skillsCtx.logger.warn("%s: skill register failed: %s", options.name, error);
|
||||
});
|
||||
};
|
||||
const remountKbSkill = () => {
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext());
|
||||
};
|
||||
remountSkills();
|
||||
remountKbSkill();
|
||||
const stopSkillWatch = watchNetxConnection(() => {
|
||||
remountSkills();
|
||||
});
|
||||
const stopKbWatch = watchKbContext(() => {
|
||||
remountKbSkill();
|
||||
});
|
||||
skillsCtx.effect(() => () => {
|
||||
skillGeneration += 1;
|
||||
stopSkillWatch();
|
||||
stopKbWatch();
|
||||
unregisterSkills?.();
|
||||
unregisterSkills = undefined;
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
}, `${options.name}: dispose skills`);
|
||||
});
|
||||
ctx.effect(() => () => {
|
||||
|
|
|
|||
627
lib/client.js
627
lib/client.js
|
|
@ -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,
|
||||
|
|
|
|||
386
lib/index.js
386
lib/index.js
|
|
@ -2,9 +2,9 @@ import { createRequire } from "node:module";
|
|||
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
||||
|
||||
// src/index.ts
|
||||
import { cpSync, existsSync as existsSync2, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { cpSync, existsSync as existsSync3, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { homedir as homedir3 } from "node:os";
|
||||
import { dirname as dirname2, join as join3 } from "node:path";
|
||||
import { dirname as dirname3, join as join4 } from "node:path";
|
||||
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
||||
import z from "@deepseek-ai/schemastery";
|
||||
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
||||
|
|
@ -785,28 +785,331 @@ async function registerGroupSkills(ctx, groupIds, providerLabel) {
|
|||
};
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.connection-store");
|
||||
// src/netx/kb-context-skill.ts
|
||||
var SKILL_NAME = "kb-context";
|
||||
function skillBody(snapshot) {
|
||||
if (snapshot.status === "configured") {
|
||||
const flags = Object.entries(snapshot.content).filter(([, on]) => on).map(([key]) => key).join(", ") || "(none)";
|
||||
return {
|
||||
description: "Operator knowledge-base context for this Host (paths + identity from MANIFEST).",
|
||||
content: [
|
||||
"## Knowledge base (configured)",
|
||||
"",
|
||||
"When troubleshooting with operator playbooks or docs, **compose paths from this root**.",
|
||||
"Do not invent another operator or country.",
|
||||
"",
|
||||
`| Field | Value |`,
|
||||
`| --- | --- |`,
|
||||
`| kbStatus | configured |`,
|
||||
`| kbRoot | \`${snapshot.realRoot}\` |`,
|
||||
`| kbOperator | ${snapshot.operatorName} |`,
|
||||
`| kbCountry | ${snapshot.country} |`,
|
||||
`| kbVersion | ${snapshot.version} |`,
|
||||
`| kbContent (on) | ${flags} |`,
|
||||
"",
|
||||
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
const reason = snapshot.status === "error" ? snapshot.errorMessage || "invalid knowledge base" : "kbRoot is empty";
|
||||
return {
|
||||
description: "Knowledge-base context is unavailable — stay on pure netx evidence.",
|
||||
content: [
|
||||
"## Knowledge base (unavailable)",
|
||||
"",
|
||||
`kbStatus=\`${snapshot.status}\`. ${reason}`,
|
||||
"",
|
||||
"**Do not invent an operator, country, or KB paths.**",
|
||||
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
|
||||
"Operator-specific playbooks are out of scope until a valid MANIFEST is configured."
|
||||
].join(`
|
||||
`)
|
||||
};
|
||||
}
|
||||
function registerKbContextSkill(ctx, snapshot) {
|
||||
const skillsApi = ctx.skills;
|
||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||
return () => {};
|
||||
}
|
||||
const body = skillBody(snapshot);
|
||||
return skillsApi.register({
|
||||
name: SKILL_NAME,
|
||||
description: body.description,
|
||||
content: body.content,
|
||||
provider: "netxops-kb",
|
||||
source: "custom"
|
||||
});
|
||||
}
|
||||
|
||||
// src/netx/kb-manifest.ts
|
||||
import { existsSync as existsSync2, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
||||
var EMPTY_CONTENT = {
|
||||
regions: false,
|
||||
theory: false,
|
||||
packet: false,
|
||||
skills: false
|
||||
};
|
||||
function unconfiguredKbSnapshot() {
|
||||
return {
|
||||
status: "unconfigured",
|
||||
realRoot: "",
|
||||
operatorName: "",
|
||||
country: "",
|
||||
version: "",
|
||||
content: { ...EMPTY_CONTENT },
|
||||
errorMessage: ""
|
||||
};
|
||||
}
|
||||
function errorSnapshot(message) {
|
||||
return {
|
||||
status: "error",
|
||||
realRoot: "",
|
||||
operatorName: "",
|
||||
country: "",
|
||||
version: "",
|
||||
content: { ...EMPTY_CONTENT },
|
||||
errorMessage: message
|
||||
};
|
||||
}
|
||||
function findManifest(kbRoot, maxDepth = 3) {
|
||||
const root = resolve(kbRoot.trim());
|
||||
if (!kbRoot.trim()) {
|
||||
return { paths: [], error: "kbRoot is empty" };
|
||||
}
|
||||
let rootStat;
|
||||
try {
|
||||
rootStat = statSync(root);
|
||||
} catch {
|
||||
return { paths: [], error: `kbRoot not found: ${root}` };
|
||||
}
|
||||
if (!rootStat.isDirectory()) {
|
||||
return { paths: [], error: `kbRoot is not a directory: ${root}` };
|
||||
}
|
||||
const direct = join3(root, "MANIFEST.json");
|
||||
if (existsSync2(direct)) {
|
||||
try {
|
||||
if (statSync(direct).isFile())
|
||||
return { paths: [direct] };
|
||||
} catch {}
|
||||
}
|
||||
const found = [];
|
||||
const walk = (dir, depth) => {
|
||||
if (depth > maxDepth)
|
||||
return;
|
||||
const candidate = join3(dir, "MANIFEST.json");
|
||||
if (existsSync2(candidate)) {
|
||||
try {
|
||||
if (statSync(candidate).isFile())
|
||||
found.push(candidate);
|
||||
} catch {}
|
||||
}
|
||||
if (depth === maxDepth)
|
||||
return;
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const name of entries) {
|
||||
if (name === "node_modules" || name === ".git")
|
||||
continue;
|
||||
const full = join3(dir, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync(full);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (st.isDirectory())
|
||||
walk(full, depth + 1);
|
||||
}
|
||||
};
|
||||
let topEntries;
|
||||
try {
|
||||
topEntries = readdirSync(root);
|
||||
} catch {
|
||||
return { paths: [], error: `cannot read kbRoot: ${root}` };
|
||||
}
|
||||
for (const name of topEntries) {
|
||||
if (name === "node_modules" || name === ".git")
|
||||
continue;
|
||||
const full = join3(root, name);
|
||||
let st;
|
||||
try {
|
||||
st = statSync(full);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (st.isDirectory())
|
||||
walk(full, 1);
|
||||
}
|
||||
return { paths: found };
|
||||
}
|
||||
function asNonEmptyString(value, field) {
|
||||
if (typeof value !== "string" || value.trim() === "") {
|
||||
throw new Error(`MANIFEST missing required string field: ${field}`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
function parseContent(raw) {
|
||||
const out = { ...EMPTY_CONTENT };
|
||||
if (raw === undefined || raw === null) {
|
||||
throw new Error("MANIFEST missing required object field: content");
|
||||
}
|
||||
if (typeof raw !== "object" || Array.isArray(raw)) {
|
||||
throw new Error("MANIFEST content must be an object");
|
||||
}
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
out[key] = value === true;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function parseManifest(raw) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
throw new Error(`MANIFEST is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
||||
throw new Error("MANIFEST root must be an object");
|
||||
}
|
||||
const row = data;
|
||||
if (row.schemaVersion !== "1.0") {
|
||||
throw new Error(`unsupported schemaVersion (want "1.0", got ${JSON.stringify(row.schemaVersion)})`);
|
||||
}
|
||||
if (row.packageType !== "operator-subset") {
|
||||
throw new Error(`unsupported packageType (want "operator-subset", got ${JSON.stringify(row.packageType)})`);
|
||||
}
|
||||
const operator = row.operator;
|
||||
if (operator === null || typeof operator !== "object" || Array.isArray(operator)) {
|
||||
throw new Error("MANIFEST missing required object field: operator");
|
||||
}
|
||||
const op = operator;
|
||||
return {
|
||||
operatorName: asNonEmptyString(op.name, "operator.name"),
|
||||
country: asNonEmptyString(op.country, "operator.country"),
|
||||
version: asNonEmptyString(row.version, "version"),
|
||||
content: parseContent(row.content)
|
||||
};
|
||||
}
|
||||
function resolveKbRoot(kbRoot, maxDepth = 3) {
|
||||
const trimmed = kbRoot.trim();
|
||||
if (!trimmed)
|
||||
return unconfiguredKbSnapshot();
|
||||
const located = findManifest(trimmed, maxDepth);
|
||||
if (located.error)
|
||||
return errorSnapshot(located.error);
|
||||
if (located.paths.length === 0) {
|
||||
return errorSnapshot(`no MANIFEST.json under ${resolve(trimmed)} (maxDepth=${maxDepth})`);
|
||||
}
|
||||
if (located.paths.length > 1) {
|
||||
return errorSnapshot(`ambiguous MANIFEST.json (${located.paths.length} hits); pick a unique package root`);
|
||||
}
|
||||
const manifestPath = located.paths[0];
|
||||
let raw;
|
||||
try {
|
||||
raw = readFileSync(manifestPath, "utf8");
|
||||
} catch (error) {
|
||||
return errorSnapshot(`cannot read MANIFEST: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
try {
|
||||
const parsed = parseManifest(raw);
|
||||
return {
|
||||
status: "configured",
|
||||
realRoot: dirname2(manifestPath),
|
||||
operatorName: parsed.operatorName,
|
||||
country: parsed.country,
|
||||
version: parsed.version,
|
||||
content: parsed.content,
|
||||
errorMessage: ""
|
||||
};
|
||||
} catch (error) {
|
||||
return errorSnapshot(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
// src/netx/kb-runtime.ts
|
||||
var STORE_KEY2 = Symbol.for("dsh-netxops.kb-store");
|
||||
var ENV_KEYS = [
|
||||
"KB_ROOT",
|
||||
"KB_OPERATOR",
|
||||
"KB_COUNTRY",
|
||||
"KB_VERSION",
|
||||
"KB_CONTENT",
|
||||
"KB_STATUS"
|
||||
];
|
||||
function store2() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY2];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
current = { snapshot: unconfiguredKbSnapshot(), listeners: new Set };
|
||||
root[STORE_KEY2] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function publishNetxConnection(next) {
|
||||
function getKbContext() {
|
||||
return { ...store2().snapshot, content: { ...store2().snapshot.content } };
|
||||
}
|
||||
function publishKbContext(next) {
|
||||
const state = store2();
|
||||
state.snapshot = {
|
||||
...next,
|
||||
content: { ...next.content }
|
||||
};
|
||||
for (const listener of state.listeners)
|
||||
listener();
|
||||
}
|
||||
function watchKbContext(listener) {
|
||||
const state = store2();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
function applyKbEnv(snapshot) {
|
||||
for (const key of ENV_KEYS) {
|
||||
delete process.env[key];
|
||||
}
|
||||
process.env.KB_STATUS = snapshot.status;
|
||||
if (snapshot.status !== "configured")
|
||||
return;
|
||||
process.env.KB_ROOT = snapshot.realRoot;
|
||||
process.env.KB_OPERATOR = snapshot.operatorName;
|
||||
process.env.KB_COUNTRY = snapshot.country;
|
||||
process.env.KB_VERSION = snapshot.version;
|
||||
process.env.KB_CONTENT = JSON.stringify(snapshot.content);
|
||||
}
|
||||
function resetKbContext() {
|
||||
publishKbContext(unconfiguredKbSnapshot());
|
||||
applyKbEnv(unconfiguredKbSnapshot());
|
||||
}
|
||||
|
||||
// src/netx/runtime.ts
|
||||
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
|
||||
function store3() {
|
||||
const root = globalThis;
|
||||
let current = root[STORE_KEY3];
|
||||
if (current === undefined) {
|
||||
current = { connection: undefined, listeners: new Set };
|
||||
root[STORE_KEY3] = current;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function publishNetxConnection(next) {
|
||||
const state = store3();
|
||||
state.connection = next;
|
||||
for (const listener of state.listeners)
|
||||
listener();
|
||||
}
|
||||
function getNetxConnection() {
|
||||
return store2().connection;
|
||||
return store3().connection;
|
||||
}
|
||||
function watchNetxConnection(listener) {
|
||||
const state = store2();
|
||||
const state = store3();
|
||||
state.listeners.add(listener);
|
||||
return () => {
|
||||
state.listeners.delete(listener);
|
||||
|
|
@ -3390,33 +3693,34 @@ var Config = z.object({
|
|||
groupOpsInPreset: z.boolean().default(true),
|
||||
groupOpsPublic: z.boolean().default(false),
|
||||
groupTopologyInPreset: z.boolean().default(false),
|
||||
groupTopologyPublic: z.boolean().default(false)
|
||||
groupTopologyPublic: z.boolean().default(false),
|
||||
kbRoot: z.string().default("")
|
||||
});
|
||||
function packageRoot() {
|
||||
return join3(dirname2(fileURLToPath2(import.meta.url)), "..");
|
||||
return join4(dirname3(fileURLToPath2(import.meta.url)), "..");
|
||||
}
|
||||
function resolveDshHome() {
|
||||
const fromEnv = process.env.DSH_HOME?.trim();
|
||||
if (fromEnv !== undefined && fromEnv.length > 0)
|
||||
return fromEnv;
|
||||
return join3(homedir3(), ".dsh");
|
||||
return join4(homedir3(), ".dsh");
|
||||
}
|
||||
function ensureAgentPresetInstalled(logger) {
|
||||
const src = join3(packageRoot(), "presets", NETXOPS_PRESET_ID);
|
||||
const composition = join3(src, "agent.cordis.yml");
|
||||
if (!existsSync2(composition)) {
|
||||
const src = join4(packageRoot(), "presets", NETXOPS_PRESET_ID);
|
||||
const composition = join4(src, "agent.cordis.yml");
|
||||
if (!existsSync3(composition)) {
|
||||
logger.warn("netxops: bundled preset missing at %s — skip user-preset install", src);
|
||||
return;
|
||||
}
|
||||
const destParent = join3(resolveDshHome(), ".agent-presets");
|
||||
const dest = join3(destParent, NETXOPS_PRESET_ID);
|
||||
const destParent = join4(resolveDshHome(), ".agent-presets");
|
||||
const dest = join4(destParent, NETXOPS_PRESET_ID);
|
||||
try {
|
||||
mkdirSync(destParent, { recursive: true });
|
||||
if (existsSync2(dest)) {
|
||||
if (existsSync3(dest)) {
|
||||
rmSync(dest, { recursive: true, force: true });
|
||||
}
|
||||
cpSync(src, dest, { recursive: true });
|
||||
writeFileSync(join3(dest, ".dsh-netxops-managed"), `${new Date().toISOString()}
|
||||
writeFileSync(join4(dest, ".dsh-netxops-managed"), `${new Date().toISOString()}
|
||||
`, "utf8");
|
||||
logger.info("netxops: agent preset installed at %s", dest);
|
||||
} catch (error) {
|
||||
|
|
@ -3508,6 +3812,14 @@ function apply(ctx, config = Config({})) {
|
|||
toolCallTimeoutMs: current.toolCallTimeoutMs,
|
||||
groups
|
||||
});
|
||||
const kb = resolveKbRoot(current.kbRoot ?? "");
|
||||
publishKbContext(kb);
|
||||
applyKbEnv(kb);
|
||||
if (kb.status === "configured") {
|
||||
ctx.logger.info("netxops: knowledge base configured → %s (%s / %s v%s)", kb.realRoot, kb.operatorName, kb.country, kb.version);
|
||||
} else if (kb.status === "error") {
|
||||
ctx.logger.warn("netxops: knowledge base error — %s", kb.errorMessage);
|
||||
}
|
||||
restartAlarmPush(current, apiUrl, token);
|
||||
if (!tokenConfigured) {
|
||||
ctx.logger.warn("netxops: published connection → %s tokenConfigured=false (set credential %s)", apiUrl, current.tokenCredentialRef);
|
||||
|
|
@ -3559,6 +3871,7 @@ function apply(ctx, config = Config({})) {
|
|||
});
|
||||
ctx.inject(["skills"], (skillsCtx) => {
|
||||
let unregisterSkills;
|
||||
let unregisterKbSkill;
|
||||
let generation2 = 0;
|
||||
const remountPublicSkills = () => {
|
||||
const gen = ++generation2;
|
||||
|
|
@ -3579,14 +3892,27 @@ function apply(ctx, config = Config({})) {
|
|||
skillsCtx.logger.warn("netxops: public skill register failed: %s", error);
|
||||
});
|
||||
};
|
||||
const remountKbSkill = () => {
|
||||
unregisterKbSkill?.();
|
||||
unregisterKbSkill = undefined;
|
||||
const snapshot = getKbContext();
|
||||
unregisterKbSkill = registerKbContextSkill(skillsCtx, snapshot);
|
||||
skillsCtx.logger.info("netxops: kb-context skill status=%s", snapshot.status);
|
||||
};
|
||||
remountPublicSkills();
|
||||
remountKbSkill();
|
||||
const stopWatch = watchNetxConnection(() => {
|
||||
remountPublicSkills();
|
||||
});
|
||||
const stopKbWatch = watchKbContext(() => {
|
||||
remountKbSkill();
|
||||
});
|
||||
skillsCtx.effect(() => () => {
|
||||
generation2 += 1;
|
||||
stopWatch();
|
||||
stopKbWatch();
|
||||
unregisterSkills?.();
|
||||
unregisterKbSkill?.();
|
||||
}, "netxops: dispose public skills");
|
||||
});
|
||||
ctx.inject(["connection"], (connCtx) => {
|
||||
|
|
@ -3596,10 +3922,17 @@ function apply(ctx, config = Config({})) {
|
|||
connCtx.logger.warn("netxops: connection.rpc.handle unavailable — alarm status UI disabled");
|
||||
} else {
|
||||
connCtx.effect(() => {
|
||||
const dispose = rpc.handle(NETXOPS_RPC_CHANNEL, async (endpoint) => {
|
||||
const dispose = rpc.handle(NETXOPS_RPC_CHANNEL, async (endpoint, payload) => {
|
||||
if (endpoint === "alarm-push.status") {
|
||||
return { ok: true, value: getAlarmPushStatus() };
|
||||
}
|
||||
if (endpoint === "kb.status") {
|
||||
return { ok: true, value: getKbContext() };
|
||||
}
|
||||
if (endpoint === "kb.resolve") {
|
||||
const path = extractKbResolvePath(payload);
|
||||
return { ok: true, value: resolveKbRoot(path) };
|
||||
}
|
||||
if (endpoint === "sessions.export.status") {
|
||||
const value = await getSessionsExportStatus(ctx);
|
||||
return { ok: true, value };
|
||||
|
|
@ -3672,8 +4005,23 @@ function apply(ctx, config = Config({})) {
|
|||
stopAlarmPush = undefined;
|
||||
resetAlarmSession();
|
||||
resetAlarmPushStatus();
|
||||
resetKbContext();
|
||||
}, "netxops: dispose host bridge");
|
||||
}
|
||||
function extractKbResolvePath(payload) {
|
||||
if (payload === null || typeof payload !== "object" || Array.isArray(payload))
|
||||
return "";
|
||||
const row = payload;
|
||||
if (typeof row.path === "string")
|
||||
return row.path;
|
||||
const args = row.args;
|
||||
if (args !== null && typeof args === "object" && !Array.isArray(args)) {
|
||||
const path = args.path;
|
||||
if (typeof path === "string")
|
||||
return path;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
export {
|
||||
name,
|
||||
inject,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue