Register KB pack skills from operator-subset _skills (P2).

Load hasSkills packs onto preset/public planes with groupKb toggles; unify content has* keys and keep netx-ops pure.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-13 11:46:31 +08:00
parent fcdd11ac59
commit 2caa3c98d7
26 changed files with 1122 additions and 106 deletions

View file

@ -237,7 +237,11 @@ function skillBody(snapshot) {
`| kbVersion | ${snapshot.version} |`,
`| kbContent (on) | ${flags} |`,
"",
"Environment mirrors: `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT`, `KB_STATUS`."
"Environment mirrors: KB_ROOT, KB_OPERATOR, KB_COUNTRY, KB_VERSION, KB_CONTENT, KB_STATUS.",
"",
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
"keep netx-ops for live netx evidence only."
].join(`
`)
};
@ -248,7 +252,7 @@ function skillBody(snapshot) {
content: [
"## Knowledge base (unavailable)",
"",
`kbStatus=\`${snapshot.status}\`. ${reason}`,
`kbStatus=${snapshot.status}. ${reason}`,
"",
"**Do not invent an operator, country, or KB paths.**",
"Use only netx tools / live evidence (alarms, inventory, CLI, topology).",
@ -272,12 +276,77 @@ function registerKbContextSkill(ctx, snapshot) {
});
}
// src/netx/kb-pack-skills.ts
import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
import { join as join2 } from "node:path";
function kbPackSkillsEnabled(snapshot) {
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
}
async function registerKbPackSkills(ctx, snapshot, options) {
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
return () => {};
}
const skillsApi = ctx.skills;
if (!skillsApi || typeof skillsApi.register !== "function") {
return () => {};
}
const root = join2(snapshot.realRoot, "_skills");
let entries;
try {
entries = await readdir2(root);
} catch {
ctx.logger.info("netxops: kb pack skills dir missing at %s — skip", root);
return () => {};
}
const disposers = [];
const provider = options.providerLabel ?? "netxops-kb-pack";
for (const entry of entries) {
const full = join2(root, entry);
let isDir = false;
try {
isDir = (await stat2(full)).isDirectory();
} catch {
continue;
}
if (!isDir)
continue;
let skill;
try {
skill = await loadSkillBundle(full);
} catch (error) {
ctx.logger.warn("netxops: kb pack skill load failed at %s: %s", full, error instanceof Error ? error.message : String(error));
continue;
}
if (!skill) {
ctx.logger.warn("netxops: kb pack skill skipped (invalid SKILL.md): %s", full);
continue;
}
disposers.push(skillsApi.register({
name: skill.name,
description: skill.description,
content: skill.content,
path: skill.path,
resourceBase: { kind: "directory", path: skill.directory },
provider,
source: "custom"
}));
}
if (disposers.length > 0) {
ctx.logger.info("netxops: kb pack skills registered count=%s root=%s provider=%s", disposers.length, root, provider);
}
return () => {
for (const dispose of disposers)
dispose();
};
}
// src/netx/kb-manifest.ts
var EMPTY_CONTENT = {
regions: false,
theory: false,
packet: false,
skills: false
hasRegions: false,
hasTheory: false,
hasPacket: false,
hasCommon: false,
hasSkills: false
};
function unconfiguredKbSnapshot() {
return {
@ -1848,6 +1917,8 @@ function applyGroupToolsPlugin(ctx, options) {
});
ctx.inject(["skills"], (skillsCtx) => {
let unregisterKbSkill;
let unregisterKbPack;
let packGeneration = 0;
const remountSkills = () => {
const gen = ++skillGeneration;
unregisterSkills?.();
@ -1870,22 +1941,47 @@ function applyGroupToolsPlugin(ctx, options) {
unregisterKbSkill = undefined;
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext());
};
const remountKbPack = () => {
const gen = ++packGeneration;
unregisterKbPack?.();
unregisterKbPack = undefined;
const connection = getNetxConnection();
const enabled = connection?.groupKbInPreset !== false;
registerKbPackSkills(skillsCtx, getKbContext(), {
enabled,
providerLabel: `${options.name}-kb-pack`
}).then((dispose) => {
if (gen !== packGeneration) {
dispose();
return;
}
unregisterKbPack = dispose;
}).catch((error) => {
skillsCtx.logger.warn("%s: kb pack skill register failed: %s", options.name, error);
});
};
remountSkills();
remountKbSkill();
remountKbPack();
const stopSkillWatch = watchNetxConnection(() => {
remountSkills();
remountKbPack();
});
const stopKbWatch = watchKbContext(() => {
remountKbSkill();
remountKbPack();
});
skillsCtx.effect(() => () => {
skillGeneration += 1;
packGeneration += 1;
stopSkillWatch();
stopKbWatch();
unregisterSkills?.();
unregisterSkills = undefined;
unregisterKbSkill?.();
unregisterKbSkill = undefined;
unregisterKbPack?.();
unregisterKbPack = undefined;
}, `${options.name}: dispose skills`);
});
ctx.effect(() => () => {