mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 02:40:45 +08:00
Register MANIFEST paths.localSkills alongside pack _skills.
Parse packaging-generated paths and load each local_skills/*/SKILL.md with the same hasSkills toggles and provider as shared packs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6dee69be31
commit
53c47fbc6a
18 changed files with 662 additions and 156 deletions
|
|
@ -8,7 +8,8 @@ Operator-subset knowledge packages are wired through **netxops settings**, not i
|
||||||
|---------|----------------|
|
|---------|----------------|
|
||||||
| `netx-ops` | Live netx evidence (alarms / inventory / CLI) — **pure netx**, no KB playbooks |
|
| `netx-ops` | Live netx evidence (alarms / inventory / CLI) — **pure netx**, no KB playbooks |
|
||||||
| `kb-context` | Identity annotation from MANIFEST (`KB_*`) |
|
| `kb-context` | Identity annotation from MANIFEST (`KB_*`) |
|
||||||
| `_skills/kb-*` | Operator KB triage / ingest / retrieve / export / package |
|
| `_skills/kb-*` | Shared operator KB triage / ingest / retrieve / export / package |
|
||||||
|
| `paths.localSkills` | Operator-local skills (alongside `_skills/`; same toggles) |
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -26,6 +27,7 @@ Operator-subset knowledge packages are wired through **netxops settings**, not i
|
||||||
| `operator.name` / `operator.country` | required non-empty strings |
|
| `operator.name` / `operator.country` | required non-empty strings |
|
||||||
| `version` | required non-empty string |
|
| `version` | required non-empty string |
|
||||||
| `content.*` | booleans; contract keys `hasRegions` / `hasTheory` / `hasPacket` / `hasCommon` / `hasSkills` (missing → `false`). Legacy short keys `regions`/`theory`/`packet`/`skills` map to `has*` when the contract key is absent |
|
| `content.*` | booleans; contract keys `hasRegions` / `hasTheory` / `hasPacket` / `hasCommon` / `hasSkills` (missing → `false`). Legacy short keys `regions`/`theory`/`packet`/`skills` map to `has*` when the contract key is absent |
|
||||||
|
| `paths.*` | optional relative dirs under package root (packaging-generated). Plugin uses `skills` (default `_skills`) and `localSkills`; missing / blank values skipped |
|
||||||
|
|
||||||
Location: `${kbRoot}/MANIFEST.json`, else recurse ≤3 levels and **exactly one** hit. Zero or multiple → error.
|
Location: `${kbRoot}/MANIFEST.json`, else recurse ≤3 levels and **exactly one** hit. Zero or multiple → error.
|
||||||
|
|
||||||
|
|
@ -37,19 +39,20 @@ Authoritative packaging prose lives in the workspace contract notes (`插件` /
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| `process.env` | `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT` (JSON of `has*`), `KB_STATUS` |
|
| `process.env` | `KB_ROOT`, `KB_OPERATOR`, `KB_COUNTRY`, `KB_VERSION`, `KB_CONTENT` (JSON of `has*`), `KB_STATUS` |
|
||||||
| Skill `kb-context` | Same identity for the model (markdown table) |
|
| Skill `kb-context` | Same identity for the model (markdown table) |
|
||||||
| Skills from `_skills/` | Registered when `configured` **and** `hasSkills` **and** the inPreset/public toggle for that plane |
|
| Skills from pack | Registered when `configured` **and** `hasSkills` **and** the inPreset/public toggle for that plane |
|
||||||
|
|
||||||
RPC (channel `/netxops`): `kb.status` (saved snapshot), `kb.resolve` with `{ path }` (preview unsaved paths).
|
RPC (channel `/netxops`): `kb.status` (saved snapshot), `kb.resolve` with `{ path }` (preview unsaved paths).
|
||||||
|
|
||||||
## Pack skill registration
|
## Pack skill registration
|
||||||
|
|
||||||
When `KB_STATUS=configured` and `content.hasSkills=true`, the plugin scans:
|
When `KB_STATUS=configured` and `content.hasSkills=true`, the plugin scans (same register rules / toggles / `provider: netxops-kb-pack`):
|
||||||
|
|
||||||
```text
|
```text
|
||||||
${realRoot}/_skills/*/SKILL.md
|
${realRoot}/${paths.skills || '_skills'}/*/SKILL.md
|
||||||
|
${realRoot}/${paths.localSkills}/*/SKILL.md # only when paths.localSkills is set
|
||||||
```
|
```
|
||||||
|
|
||||||
and registers each valid frontmatter skill (`provider: netxops-kb-pack`).
|
`paths.localSkills` missing, blank, or an empty/missing directory → skipped (ops continues). Invalid `SKILL.md` bundles are skipped the same way as under `_skills/`.
|
||||||
|
|
||||||
| Setting | Default | Plane |
|
| Setting | Default | Plane |
|
||||||
|---------|---------|-------|
|
|---------|---------|-------|
|
||||||
|
|
@ -65,6 +68,6 @@ Invalid or missing `_skills` directories are skipped (ops continues).
|
||||||
| Empty `kbRoot` | `unconfigured` | Pure netx; no invented operator; no kb-* packs |
|
| Empty `kbRoot` | `unconfigured` | Pure netx; no invented operator; no kb-* packs |
|
||||||
| Missing / ambiguous / invalid MANIFEST | `error` | Pure netx; badge shows error detail |
|
| Missing / ambiguous / invalid MANIFEST | `error` | Pure netx; badge shows error detail |
|
||||||
| Valid MANIFEST, `hasSkills=false` | `configured` | Identity only (`kb-context` + env) |
|
| Valid MANIFEST, `hasSkills=false` | `configured` | Identity only (`kb-context` + env) |
|
||||||
| Valid + `hasSkills` + toggles | `configured` | Identity + `_skills/kb-*` on the enabled plane(s) |
|
| Valid + `hasSkills` + toggles | `configured` | Identity + pack skills (`_skills` / `paths.skills`) and optional `paths.localSkills` on the enabled plane(s) |
|
||||||
|
|
||||||
**Never** invent an operator when status is not `configured`.
|
**Never** invent an operator when status is not `configured`.
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,8 @@ function skillBody(snapshot) {
|
||||||
"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",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
||||||
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
|
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
||||||
|
"plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -282,24 +283,29 @@ import { join as join2 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
async function registerKbPackSkills(ctx, snapshot, options) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return () => {};
|
return join2(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
const skillsApi = ctx.skills;
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
return () => {};
|
if (!rel)
|
||||||
}
|
return null;
|
||||||
const root = join2(snapshot.realRoot, "_skills");
|
return join2(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = await readdir2(root);
|
entries = await readdir2(root);
|
||||||
} catch {
|
} catch {
|
||||||
ctx.logger.info("netxops: kb pack skills dir missing at %s — skip", root);
|
if (options.logMissing) {
|
||||||
return () => {};
|
ctx.logger.info("netxops: kb %s dir missing at %s — skip", options.label, root);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
if (entries.length === 0)
|
||||||
|
return [];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const provider = options.providerLabel ?? "netxops-kb-pack";
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join2(root, entry);
|
const full = join2(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -314,11 +320,11 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
try {
|
try {
|
||||||
skill = await loadSkillBundle(full);
|
skill = await loadSkillBundle(full);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.warn("netxops: kb pack skill load failed at %s: %s", full, error instanceof Error ? error.message : String(error));
|
ctx.logger.warn("netxops: kb %s skill load failed at %s: %s", options.label, full, error instanceof Error ? error.message : String(error));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!skill) {
|
if (!skill) {
|
||||||
ctx.logger.warn("netxops: kb pack skill skipped (invalid SKILL.md): %s", full);
|
ctx.logger.warn("netxops: kb %s skill skipped (invalid SKILL.md): %s", options.label, full);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
|
|
@ -331,8 +337,32 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return disposers;
|
||||||
|
}
|
||||||
|
async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
|
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const skillsApi = ctx.skills;
|
||||||
|
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const provider = options.providerLabel ?? "netxops-kb-pack";
|
||||||
|
const disposers = [];
|
||||||
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
|
logMissing: true,
|
||||||
|
label: "pack"
|
||||||
|
}));
|
||||||
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
if (localRoot) {
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
|
logMissing: false,
|
||||||
|
label: "local"
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (disposers.length > 0) {
|
if (disposers.length > 0) {
|
||||||
ctx.logger.info("netxops: kb pack skills registered count=%s root=%s provider=%s", disposers.length, root, provider);
|
ctx.logger.info("netxops: kb pack skills registered count=%s skillsRoot=%s localSkills=%s provider=%s", disposers.length, skillsRoot, localRoot ?? "(none)", provider);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
|
|
@ -356,6 +386,7 @@ function unconfiguredKbSnapshot() {
|
||||||
country: "",
|
country: "",
|
||||||
version: "",
|
version: "",
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: ""
|
errorMessage: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +403,12 @@ function store() {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
function getKbContext() {
|
function getKbContext() {
|
||||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
const snap = store().snapshot;
|
||||||
|
return {
|
||||||
|
...snap,
|
||||||
|
content: { ...snap.content },
|
||||||
|
paths: { ...snap.paths }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function watchKbContext(listener) {
|
function watchKbContext(listener) {
|
||||||
const state = store();
|
const state = store();
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,8 @@ function skillBody(snapshot) {
|
||||||
"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",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
||||||
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
|
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
||||||
|
"plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -282,24 +283,29 @@ import { join as join2 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
async function registerKbPackSkills(ctx, snapshot, options) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return () => {};
|
return join2(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
const skillsApi = ctx.skills;
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
return () => {};
|
if (!rel)
|
||||||
}
|
return null;
|
||||||
const root = join2(snapshot.realRoot, "_skills");
|
return join2(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = await readdir2(root);
|
entries = await readdir2(root);
|
||||||
} catch {
|
} catch {
|
||||||
ctx.logger.info("netxops: kb pack skills dir missing at %s — skip", root);
|
if (options.logMissing) {
|
||||||
return () => {};
|
ctx.logger.info("netxops: kb %s dir missing at %s — skip", options.label, root);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
if (entries.length === 0)
|
||||||
|
return [];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const provider = options.providerLabel ?? "netxops-kb-pack";
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join2(root, entry);
|
const full = join2(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -314,11 +320,11 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
try {
|
try {
|
||||||
skill = await loadSkillBundle(full);
|
skill = await loadSkillBundle(full);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.warn("netxops: kb pack skill load failed at %s: %s", full, error instanceof Error ? error.message : String(error));
|
ctx.logger.warn("netxops: kb %s skill load failed at %s: %s", options.label, full, error instanceof Error ? error.message : String(error));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!skill) {
|
if (!skill) {
|
||||||
ctx.logger.warn("netxops: kb pack skill skipped (invalid SKILL.md): %s", full);
|
ctx.logger.warn("netxops: kb %s skill skipped (invalid SKILL.md): %s", options.label, full);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
|
|
@ -331,8 +337,32 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return disposers;
|
||||||
|
}
|
||||||
|
async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
|
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const skillsApi = ctx.skills;
|
||||||
|
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const provider = options.providerLabel ?? "netxops-kb-pack";
|
||||||
|
const disposers = [];
|
||||||
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
|
logMissing: true,
|
||||||
|
label: "pack"
|
||||||
|
}));
|
||||||
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
if (localRoot) {
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
|
logMissing: false,
|
||||||
|
label: "local"
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (disposers.length > 0) {
|
if (disposers.length > 0) {
|
||||||
ctx.logger.info("netxops: kb pack skills registered count=%s root=%s provider=%s", disposers.length, root, provider);
|
ctx.logger.info("netxops: kb pack skills registered count=%s skillsRoot=%s localSkills=%s provider=%s", disposers.length, skillsRoot, localRoot ?? "(none)", provider);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
|
|
@ -356,6 +386,7 @@ function unconfiguredKbSnapshot() {
|
||||||
country: "",
|
country: "",
|
||||||
version: "",
|
version: "",
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: ""
|
errorMessage: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +403,12 @@ function store() {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
function getKbContext() {
|
function getKbContext() {
|
||||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
const snap = store().snapshot;
|
||||||
|
return {
|
||||||
|
...snap,
|
||||||
|
content: { ...snap.content },
|
||||||
|
paths: { ...snap.paths }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function watchKbContext(listener) {
|
function watchKbContext(listener) {
|
||||||
const state = store();
|
const state = store();
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,8 @@ function skillBody(snapshot) {
|
||||||
"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",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
||||||
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
|
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
||||||
|
"plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -282,24 +283,29 @@ import { join as join2 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
async function registerKbPackSkills(ctx, snapshot, options) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return () => {};
|
return join2(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
const skillsApi = ctx.skills;
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
return () => {};
|
if (!rel)
|
||||||
}
|
return null;
|
||||||
const root = join2(snapshot.realRoot, "_skills");
|
return join2(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = await readdir2(root);
|
entries = await readdir2(root);
|
||||||
} catch {
|
} catch {
|
||||||
ctx.logger.info("netxops: kb pack skills dir missing at %s — skip", root);
|
if (options.logMissing) {
|
||||||
return () => {};
|
ctx.logger.info("netxops: kb %s dir missing at %s — skip", options.label, root);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
if (entries.length === 0)
|
||||||
|
return [];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const provider = options.providerLabel ?? "netxops-kb-pack";
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join2(root, entry);
|
const full = join2(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -314,11 +320,11 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
try {
|
try {
|
||||||
skill = await loadSkillBundle(full);
|
skill = await loadSkillBundle(full);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.warn("netxops: kb pack skill load failed at %s: %s", full, error instanceof Error ? error.message : String(error));
|
ctx.logger.warn("netxops: kb %s skill load failed at %s: %s", options.label, full, error instanceof Error ? error.message : String(error));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!skill) {
|
if (!skill) {
|
||||||
ctx.logger.warn("netxops: kb pack skill skipped (invalid SKILL.md): %s", full);
|
ctx.logger.warn("netxops: kb %s skill skipped (invalid SKILL.md): %s", options.label, full);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
|
|
@ -331,8 +337,32 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return disposers;
|
||||||
|
}
|
||||||
|
async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
|
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const skillsApi = ctx.skills;
|
||||||
|
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const provider = options.providerLabel ?? "netxops-kb-pack";
|
||||||
|
const disposers = [];
|
||||||
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
|
logMissing: true,
|
||||||
|
label: "pack"
|
||||||
|
}));
|
||||||
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
if (localRoot) {
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
|
logMissing: false,
|
||||||
|
label: "local"
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (disposers.length > 0) {
|
if (disposers.length > 0) {
|
||||||
ctx.logger.info("netxops: kb pack skills registered count=%s root=%s provider=%s", disposers.length, root, provider);
|
ctx.logger.info("netxops: kb pack skills registered count=%s skillsRoot=%s localSkills=%s provider=%s", disposers.length, skillsRoot, localRoot ?? "(none)", provider);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
|
|
@ -356,6 +386,7 @@ function unconfiguredKbSnapshot() {
|
||||||
country: "",
|
country: "",
|
||||||
version: "",
|
version: "",
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: ""
|
errorMessage: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +403,12 @@ function store() {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
function getKbContext() {
|
function getKbContext() {
|
||||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
const snap = store().snapshot;
|
||||||
|
return {
|
||||||
|
...snap,
|
||||||
|
content: { ...snap.content },
|
||||||
|
paths: { ...snap.paths }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function watchKbContext(listener) {
|
function watchKbContext(listener) {
|
||||||
const state = store();
|
const state = store();
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,8 @@ function skillBody(snapshot) {
|
||||||
"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",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
||||||
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
|
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
||||||
|
"plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -282,24 +283,29 @@ import { join as join2 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
async function registerKbPackSkills(ctx, snapshot, options) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return () => {};
|
return join2(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
const skillsApi = ctx.skills;
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
return () => {};
|
if (!rel)
|
||||||
}
|
return null;
|
||||||
const root = join2(snapshot.realRoot, "_skills");
|
return join2(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = await readdir2(root);
|
entries = await readdir2(root);
|
||||||
} catch {
|
} catch {
|
||||||
ctx.logger.info("netxops: kb pack skills dir missing at %s — skip", root);
|
if (options.logMissing) {
|
||||||
return () => {};
|
ctx.logger.info("netxops: kb %s dir missing at %s — skip", options.label, root);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
if (entries.length === 0)
|
||||||
|
return [];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const provider = options.providerLabel ?? "netxops-kb-pack";
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join2(root, entry);
|
const full = join2(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -314,11 +320,11 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
try {
|
try {
|
||||||
skill = await loadSkillBundle(full);
|
skill = await loadSkillBundle(full);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.warn("netxops: kb pack skill load failed at %s: %s", full, error instanceof Error ? error.message : String(error));
|
ctx.logger.warn("netxops: kb %s skill load failed at %s: %s", options.label, full, error instanceof Error ? error.message : String(error));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!skill) {
|
if (!skill) {
|
||||||
ctx.logger.warn("netxops: kb pack skill skipped (invalid SKILL.md): %s", full);
|
ctx.logger.warn("netxops: kb %s skill skipped (invalid SKILL.md): %s", options.label, full);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
|
|
@ -331,8 +337,32 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return disposers;
|
||||||
|
}
|
||||||
|
async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
|
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const skillsApi = ctx.skills;
|
||||||
|
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const provider = options.providerLabel ?? "netxops-kb-pack";
|
||||||
|
const disposers = [];
|
||||||
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
|
logMissing: true,
|
||||||
|
label: "pack"
|
||||||
|
}));
|
||||||
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
if (localRoot) {
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
|
logMissing: false,
|
||||||
|
label: "local"
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (disposers.length > 0) {
|
if (disposers.length > 0) {
|
||||||
ctx.logger.info("netxops: kb pack skills registered count=%s root=%s provider=%s", disposers.length, root, provider);
|
ctx.logger.info("netxops: kb pack skills registered count=%s skillsRoot=%s localSkills=%s provider=%s", disposers.length, skillsRoot, localRoot ?? "(none)", provider);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
|
|
@ -356,6 +386,7 @@ function unconfiguredKbSnapshot() {
|
||||||
country: "",
|
country: "",
|
||||||
version: "",
|
version: "",
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: ""
|
errorMessage: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +403,12 @@ function store() {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
function getKbContext() {
|
function getKbContext() {
|
||||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
const snap = store().snapshot;
|
||||||
|
return {
|
||||||
|
...snap,
|
||||||
|
content: { ...snap.content },
|
||||||
|
paths: { ...snap.paths }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function watchKbContext(listener) {
|
function watchKbContext(listener) {
|
||||||
const state = store();
|
const state = store();
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,8 @@ function skillBody(snapshot) {
|
||||||
"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",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
||||||
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
|
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
||||||
|
"plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -282,24 +283,29 @@ import { join as join2 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
async function registerKbPackSkills(ctx, snapshot, options) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return () => {};
|
return join2(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
const skillsApi = ctx.skills;
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
return () => {};
|
if (!rel)
|
||||||
}
|
return null;
|
||||||
const root = join2(snapshot.realRoot, "_skills");
|
return join2(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = await readdir2(root);
|
entries = await readdir2(root);
|
||||||
} catch {
|
} catch {
|
||||||
ctx.logger.info("netxops: kb pack skills dir missing at %s — skip", root);
|
if (options.logMissing) {
|
||||||
return () => {};
|
ctx.logger.info("netxops: kb %s dir missing at %s — skip", options.label, root);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
if (entries.length === 0)
|
||||||
|
return [];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const provider = options.providerLabel ?? "netxops-kb-pack";
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join2(root, entry);
|
const full = join2(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -314,11 +320,11 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
try {
|
try {
|
||||||
skill = await loadSkillBundle(full);
|
skill = await loadSkillBundle(full);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.warn("netxops: kb pack skill load failed at %s: %s", full, error instanceof Error ? error.message : String(error));
|
ctx.logger.warn("netxops: kb %s skill load failed at %s: %s", options.label, full, error instanceof Error ? error.message : String(error));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!skill) {
|
if (!skill) {
|
||||||
ctx.logger.warn("netxops: kb pack skill skipped (invalid SKILL.md): %s", full);
|
ctx.logger.warn("netxops: kb %s skill skipped (invalid SKILL.md): %s", options.label, full);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
|
|
@ -331,8 +337,32 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return disposers;
|
||||||
|
}
|
||||||
|
async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
|
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const skillsApi = ctx.skills;
|
||||||
|
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const provider = options.providerLabel ?? "netxops-kb-pack";
|
||||||
|
const disposers = [];
|
||||||
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
|
logMissing: true,
|
||||||
|
label: "pack"
|
||||||
|
}));
|
||||||
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
if (localRoot) {
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
|
logMissing: false,
|
||||||
|
label: "local"
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (disposers.length > 0) {
|
if (disposers.length > 0) {
|
||||||
ctx.logger.info("netxops: kb pack skills registered count=%s root=%s provider=%s", disposers.length, root, provider);
|
ctx.logger.info("netxops: kb pack skills registered count=%s skillsRoot=%s localSkills=%s provider=%s", disposers.length, skillsRoot, localRoot ?? "(none)", provider);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
|
|
@ -356,6 +386,7 @@ function unconfiguredKbSnapshot() {
|
||||||
country: "",
|
country: "",
|
||||||
version: "",
|
version: "",
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: ""
|
errorMessage: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +403,12 @@ function store() {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
function getKbContext() {
|
function getKbContext() {
|
||||||
return { ...store().snapshot, content: { ...store().snapshot.content } };
|
const snap = store().snapshot;
|
||||||
|
return {
|
||||||
|
...snap,
|
||||||
|
content: { ...snap.content },
|
||||||
|
paths: { ...snap.paths }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function watchKbContext(listener) {
|
function watchKbContext(listener) {
|
||||||
const state = store();
|
const state = store();
|
||||||
|
|
|
||||||
|
|
@ -583,6 +583,7 @@ function unconfiguredKbSnapshot() {
|
||||||
country: "",
|
country: "",
|
||||||
version: "",
|
version: "",
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: ""
|
errorMessage: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -604,6 +605,14 @@ function asKbSnapshot(value) {
|
||||||
content[key] = flag === true;
|
content[key] = flag === true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const pathsRaw = row.paths;
|
||||||
|
const paths = {};
|
||||||
|
if (pathsRaw !== null && typeof pathsRaw === "object" && !Array.isArray(pathsRaw)) {
|
||||||
|
for (const [key, value2] of Object.entries(pathsRaw)) {
|
||||||
|
if (typeof value2 === "string" && value2.trim())
|
||||||
|
paths[key] = value2.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
realRoot: typeof row.realRoot === "string" ? row.realRoot : "",
|
realRoot: typeof row.realRoot === "string" ? row.realRoot : "",
|
||||||
|
|
@ -611,6 +620,7 @@ function asKbSnapshot(value) {
|
||||||
country: typeof row.country === "string" ? row.country : "",
|
country: typeof row.country === "string" ? row.country : "",
|
||||||
version: typeof row.version === "string" ? row.version : "",
|
version: typeof row.version === "string" ? row.version : "",
|
||||||
content,
|
content,
|
||||||
|
paths,
|
||||||
errorMessage: typeof row.errorMessage === "string" ? row.errorMessage : ""
|
errorMessage: typeof row.errorMessage === "string" ? row.errorMessage : ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -2282,7 +2292,7 @@ var en = {
|
||||||
groupInPreset: "In Netx Ops preset",
|
groupInPreset: "In Netx Ops preset",
|
||||||
groupPublic: "Publish to other presets",
|
groupPublic: "Publish to other presets",
|
||||||
kbRoot: "Package root",
|
kbRoot: "Package root",
|
||||||
kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from _skills/ when hasSkills.",
|
kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from paths.skills/_skills and paths.localSkills when hasSkills.",
|
||||||
kbBrowse: "Browse…",
|
kbBrowse: "Browse…",
|
||||||
kbBrowseUnavailable: "Directory picker unavailable — paste an absolute folder path, then Save.",
|
kbBrowseUnavailable: "Directory picker unavailable — paste an absolute folder path, then Save.",
|
||||||
kbBrowseFailed: "Browse failed: {detail}",
|
kbBrowseFailed: "Browse failed: {detail}",
|
||||||
|
|
@ -2355,7 +2365,7 @@ var zh = {
|
||||||
groupInPreset: "在 Netx Ops 预设中启用",
|
groupInPreset: "在 Netx Ops 预设中启用",
|
||||||
groupPublic: "对其他预设公开",
|
groupPublic: "对其他预设公开",
|
||||||
kbRoot: "知识包根目录",
|
kbRoot: "知识包根目录",
|
||||||
kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 _skills/。",
|
kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。",
|
||||||
kbBrowse: "浏览…",
|
kbBrowse: "浏览…",
|
||||||
kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对目录路径后点保存。",
|
kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对目录路径后点保存。",
|
||||||
kbBrowseFailed: "浏览失败:{detail}",
|
kbBrowseFailed: "浏览失败:{detail}",
|
||||||
|
|
|
||||||
95
lib/index.js
95
lib/index.js
|
|
@ -810,7 +810,8 @@ function skillBody(snapshot) {
|
||||||
"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",
|
"Business playbooks (kb-troubleshoot, kb-retrieve, …) register from",
|
||||||
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
|
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
||||||
|
"plus paths.localSkills when present — use those skills for KB triage;",
|
||||||
"keep netx-ops for live netx evidence only."
|
"keep netx-ops for live netx evidence only."
|
||||||
].join(`
|
].join(`
|
||||||
`)
|
`)
|
||||||
|
|
@ -871,6 +872,7 @@ function unconfiguredKbSnapshot() {
|
||||||
country: "",
|
country: "",
|
||||||
version: "",
|
version: "",
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: ""
|
errorMessage: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -882,6 +884,7 @@ function errorSnapshot(message) {
|
||||||
country: "",
|
country: "",
|
||||||
version: "",
|
version: "",
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: message
|
errorMessage: message
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -988,6 +991,23 @@ function parseContent(raw) {
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
function parsePaths(raw) {
|
||||||
|
if (raw === undefined || raw === null)
|
||||||
|
return {};
|
||||||
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
||||||
|
throw new Error("MANIFEST paths must be an object");
|
||||||
|
}
|
||||||
|
const out = {};
|
||||||
|
for (const [key, value] of Object.entries(raw)) {
|
||||||
|
if (typeof value !== "string")
|
||||||
|
continue;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed)
|
||||||
|
continue;
|
||||||
|
out[key] = trimmed;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
function parseManifest(raw) {
|
function parseManifest(raw) {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
|
|
@ -1014,7 +1034,8 @@ function parseManifest(raw) {
|
||||||
operatorName: asNonEmptyString(op.name, "operator.name"),
|
operatorName: asNonEmptyString(op.name, "operator.name"),
|
||||||
country: asNonEmptyString(op.country, "operator.country"),
|
country: asNonEmptyString(op.country, "operator.country"),
|
||||||
version: asNonEmptyString(row.version, "version"),
|
version: asNonEmptyString(row.version, "version"),
|
||||||
content: parseContent(row.content)
|
content: parseContent(row.content),
|
||||||
|
paths: parsePaths(row.paths)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function resolveKbRoot(kbRoot, maxDepth = 3) {
|
function resolveKbRoot(kbRoot, maxDepth = 3) {
|
||||||
|
|
@ -1061,6 +1082,7 @@ function resolveKbRoot(kbRoot, maxDepth = 3) {
|
||||||
country: parsed.country,
|
country: parsed.country,
|
||||||
version: parsed.version,
|
version: parsed.version,
|
||||||
content: parsed.content,
|
content: parsed.content,
|
||||||
|
paths: { ...parsed.paths },
|
||||||
errorMessage: ""
|
errorMessage: ""
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -1074,24 +1096,29 @@ import { join as join4 } from "node:path";
|
||||||
function kbPackSkillsEnabled(snapshot) {
|
function kbPackSkillsEnabled(snapshot) {
|
||||||
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
return snapshot.status === "configured" && snapshot.content.hasSkills === true;
|
||||||
}
|
}
|
||||||
async function registerKbPackSkills(ctx, snapshot, options) {
|
function resolveKbSkillsRoot(snapshot) {
|
||||||
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
const rel = snapshot.paths.skills?.trim() || "_skills";
|
||||||
return () => {};
|
return join4(snapshot.realRoot, rel);
|
||||||
}
|
}
|
||||||
const skillsApi = ctx.skills;
|
function resolveKbLocalSkillsRoot(snapshot) {
|
||||||
if (!skillsApi || typeof skillsApi.register !== "function") {
|
const rel = snapshot.paths.localSkills?.trim();
|
||||||
return () => {};
|
if (!rel)
|
||||||
}
|
return null;
|
||||||
const root = join4(snapshot.realRoot, "_skills");
|
return join4(snapshot.realRoot, rel);
|
||||||
|
}
|
||||||
|
async function registerSkillsUnderRoot(ctx, skillsApi, root, provider, options) {
|
||||||
let entries;
|
let entries;
|
||||||
try {
|
try {
|
||||||
entries = await readdir2(root);
|
entries = await readdir2(root);
|
||||||
} catch {
|
} catch {
|
||||||
ctx.logger.info("netxops: kb pack skills dir missing at %s — skip", root);
|
if (options.logMissing) {
|
||||||
return () => {};
|
ctx.logger.info("netxops: kb %s dir missing at %s — skip", options.label, root);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
if (entries.length === 0)
|
||||||
|
return [];
|
||||||
const disposers = [];
|
const disposers = [];
|
||||||
const provider = options.providerLabel ?? "netxops-kb-pack";
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join4(root, entry);
|
const full = join4(root, entry);
|
||||||
let isDir = false;
|
let isDir = false;
|
||||||
|
|
@ -1106,11 +1133,11 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
try {
|
try {
|
||||||
skill = await loadSkillBundle(full);
|
skill = await loadSkillBundle(full);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.warn("netxops: kb pack skill load failed at %s: %s", full, error instanceof Error ? error.message : String(error));
|
ctx.logger.warn("netxops: kb %s skill load failed at %s: %s", options.label, full, error instanceof Error ? error.message : String(error));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!skill) {
|
if (!skill) {
|
||||||
ctx.logger.warn("netxops: kb pack skill skipped (invalid SKILL.md): %s", full);
|
ctx.logger.warn("netxops: kb %s skill skipped (invalid SKILL.md): %s", options.label, full);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
|
|
@ -1123,8 +1150,32 @@ async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
source: "custom"
|
source: "custom"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
return disposers;
|
||||||
|
}
|
||||||
|
async function registerKbPackSkills(ctx, snapshot, options) {
|
||||||
|
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const skillsApi = ctx.skills;
|
||||||
|
if (!skillsApi || typeof skillsApi.register !== "function") {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
const provider = options.providerLabel ?? "netxops-kb-pack";
|
||||||
|
const disposers = [];
|
||||||
|
const skillsRoot = resolveKbSkillsRoot(snapshot);
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
|
logMissing: true,
|
||||||
|
label: "pack"
|
||||||
|
}));
|
||||||
|
const localRoot = resolveKbLocalSkillsRoot(snapshot);
|
||||||
|
if (localRoot) {
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
|
logMissing: false,
|
||||||
|
label: "local"
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (disposers.length > 0) {
|
if (disposers.length > 0) {
|
||||||
ctx.logger.info("netxops: kb pack skills registered count=%s root=%s provider=%s", disposers.length, root, provider);
|
ctx.logger.info("netxops: kb pack skills registered count=%s skillsRoot=%s localSkills=%s provider=%s", disposers.length, skillsRoot, localRoot ?? "(none)", provider);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
for (const dispose of disposers)
|
for (const dispose of disposers)
|
||||||
|
|
@ -1152,13 +1203,19 @@ function store2() {
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
function getKbContext() {
|
function getKbContext() {
|
||||||
return { ...store2().snapshot, content: { ...store2().snapshot.content } };
|
const snap = store2().snapshot;
|
||||||
|
return {
|
||||||
|
...snap,
|
||||||
|
content: { ...snap.content },
|
||||||
|
paths: { ...snap.paths }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function publishKbContext(next) {
|
function publishKbContext(next) {
|
||||||
const state = store2();
|
const state = store2();
|
||||||
state.snapshot = {
|
state.snapshot = {
|
||||||
...next,
|
...next,
|
||||||
content: { ...next.content }
|
content: { ...next.content },
|
||||||
|
paths: { ...next.paths }
|
||||||
};
|
};
|
||||||
for (const listener of state.listeners)
|
for (const listener of state.listeners)
|
||||||
listener();
|
listener();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-netxops",
|
"name": "dsh-netxops",
|
||||||
"version": "0.1.30",
|
"version": "0.1.30",
|
||||||
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills",
|
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": false,
|
"private": false,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ export function asKbSnapshot(value: unknown): KbSnapshot {
|
||||||
content[key] = flag === true
|
content[key] = flag === true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const pathsRaw = row.paths
|
||||||
|
const paths: Record<string, string> = {}
|
||||||
|
if (pathsRaw !== null && typeof pathsRaw === 'object' && !Array.isArray(pathsRaw)) {
|
||||||
|
for (const [key, value] of Object.entries(pathsRaw as Record<string, unknown>)) {
|
||||||
|
if (typeof value === 'string' && value.trim()) paths[key] = value.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
realRoot: typeof row.realRoot === 'string' ? row.realRoot : '',
|
realRoot: typeof row.realRoot === 'string' ? row.realRoot : '',
|
||||||
|
|
@ -38,6 +45,7 @@ export function asKbSnapshot(value: unknown): KbSnapshot {
|
||||||
country: typeof row.country === 'string' ? row.country : '',
|
country: typeof row.country === 'string' ? row.country : '',
|
||||||
version: typeof row.version === 'string' ? row.version : '',
|
version: typeof row.version === 'string' ? row.version : '',
|
||||||
content,
|
content,
|
||||||
|
paths,
|
||||||
errorMessage: typeof row.errorMessage === 'string' ? row.errorMessage : '',
|
errorMessage: typeof row.errorMessage === 'string' ? row.errorMessage : '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
||||||
groupInPreset: 'In Netx Ops preset',
|
groupInPreset: 'In Netx Ops preset',
|
||||||
groupPublic: 'Publish to other presets',
|
groupPublic: 'Publish to other presets',
|
||||||
kbRoot: 'Package root',
|
kbRoot: 'Package root',
|
||||||
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from _skills/ when hasSkills.',
|
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves. Pack skills load from paths.skills/_skills and paths.localSkills when hasSkills.',
|
||||||
kbBrowse: 'Browse…',
|
kbBrowse: 'Browse…',
|
||||||
kbBrowseUnavailable: 'Directory picker unavailable — paste an absolute folder path, then Save.',
|
kbBrowseUnavailable: 'Directory picker unavailable — paste an absolute folder path, then Save.',
|
||||||
kbBrowseFailed: 'Browse failed: {detail}',
|
kbBrowseFailed: 'Browse failed: {detail}',
|
||||||
|
|
@ -168,7 +168,7 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
||||||
groupInPreset: '在 Netx Ops 预设中启用',
|
groupInPreset: '在 Netx Ops 预设中启用',
|
||||||
groupPublic: '对其他预设公开',
|
groupPublic: '对其他预设公开',
|
||||||
kbRoot: '知识包根目录',
|
kbRoot: '知识包根目录',
|
||||||
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 _skills/。',
|
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存;hasSkills 时加载 paths.skills/_skills 与 paths.localSkills。',
|
||||||
kbBrowse: '浏览…',
|
kbBrowse: '浏览…',
|
||||||
kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。',
|
kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。',
|
||||||
kbBrowseFailed: '浏览失败:{detail}',
|
kbBrowseFailed: '浏览失败:{detail}',
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ function skillBody(snapshot: KbSnapshot): { description: string; content: string
|
||||||
'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',
|
'Business playbooks (kb-troubleshoot, kb-retrieve, …) register from',
|
||||||
`${snapshot.realRoot}/_skills/ when hasSkills is true — use those skills for KB triage;`,
|
`${snapshot.realRoot}/_skills/ (or MANIFEST paths.skills) when hasSkills is true,`,
|
||||||
|
'plus paths.localSkills when present — use those skills for KB triage;',
|
||||||
'keep netx-ops for live netx evidence only.',
|
'keep netx-ops for live netx evidence only.',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
* - packageType === "operator-subset"
|
* - packageType === "operator-subset"
|
||||||
* - operator.name / operator.country, version required
|
* - operator.name / operator.country, version required
|
||||||
* - content.* boolean flags (missing → false)
|
* - content.* boolean flags (missing → false)
|
||||||
|
* - paths.* optional relative dirs (packaging-generated; missing keys skipped)
|
||||||
*
|
*
|
||||||
* Location: `${kbRoot}/MANIFEST.json`, else recurse ≤ maxDepth and accept
|
* Location: `${kbRoot}/MANIFEST.json`, else recurse ≤ maxDepth and accept
|
||||||
* exactly one hit (0 or >1 → error).
|
* exactly one hit (0 or >1 → error).
|
||||||
|
|
@ -24,6 +25,18 @@ export interface KbContentFlags {
|
||||||
[key: string]: boolean
|
[key: string]: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Packaging-generated relative paths under the package root.
|
||||||
|
* Plugin uses `skills` / `localSkills` for skill registration; other keys are preserved.
|
||||||
|
*/
|
||||||
|
export interface KbPaths {
|
||||||
|
/** Shared skill packs (default `_skills` when absent). */
|
||||||
|
skills?: string
|
||||||
|
/** Operator-local skills (optional; missing/empty → skip). */
|
||||||
|
localSkills?: string
|
||||||
|
[key: string]: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
export type KbStatus = 'unconfigured' | 'configured' | 'error'
|
export type KbStatus = 'unconfigured' | 'configured' | 'error'
|
||||||
|
|
||||||
export interface KbSnapshot {
|
export interface KbSnapshot {
|
||||||
|
|
@ -34,6 +47,8 @@ export interface KbSnapshot {
|
||||||
country: string
|
country: string
|
||||||
version: string
|
version: string
|
||||||
content: KbContentFlags
|
content: KbContentFlags
|
||||||
|
/** Relative paths from MANIFEST `paths` (empty when absent). */
|
||||||
|
paths: KbPaths
|
||||||
errorMessage: string
|
errorMessage: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +78,7 @@ export function unconfiguredKbSnapshot(): KbSnapshot {
|
||||||
country: '',
|
country: '',
|
||||||
version: '',
|
version: '',
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,6 +91,7 @@ function errorSnapshot(message: string): KbSnapshot {
|
||||||
country: '',
|
country: '',
|
||||||
version: '',
|
version: '',
|
||||||
content: { ...EMPTY_CONTENT },
|
content: { ...EMPTY_CONTENT },
|
||||||
|
paths: {},
|
||||||
errorMessage: message,
|
errorMessage: message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -191,6 +208,25 @@ function parseContent(raw: unknown): KbContentFlags {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse optional MANIFEST `paths` map (relative dirs under package root).
|
||||||
|
* Missing / null → {}; non-string or blank values skipped; invalid type → error.
|
||||||
|
*/
|
||||||
|
export function parsePaths(raw: unknown): KbPaths {
|
||||||
|
if (raw === undefined || raw === null) return {}
|
||||||
|
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
||||||
|
throw new Error('MANIFEST paths must be an object')
|
||||||
|
}
|
||||||
|
const out: KbPaths = {}
|
||||||
|
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
||||||
|
if (typeof value !== 'string') continue
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) continue
|
||||||
|
out[key] = trimmed
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse and validate MANIFEST JSON text (UTF-8).
|
* Parse and validate MANIFEST JSON text (UTF-8).
|
||||||
* @throws Error with a short message when invalid.
|
* @throws Error with a short message when invalid.
|
||||||
|
|
@ -200,6 +236,7 @@ export function parseManifest(raw: string): {
|
||||||
country: string
|
country: string
|
||||||
version: string
|
version: string
|
||||||
content: KbContentFlags
|
content: KbContentFlags
|
||||||
|
paths: KbPaths
|
||||||
} {
|
} {
|
||||||
let data: unknown
|
let data: unknown
|
||||||
try {
|
try {
|
||||||
|
|
@ -233,6 +270,7 @@ export function parseManifest(raw: string): {
|
||||||
country: asNonEmptyString(op.country, 'operator.country'),
|
country: asNonEmptyString(op.country, 'operator.country'),
|
||||||
version: asNonEmptyString(row.version, 'version'),
|
version: asNonEmptyString(row.version, 'version'),
|
||||||
content: parseContent(row.content),
|
content: parseContent(row.content),
|
||||||
|
paths: parsePaths(row.paths),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,6 +328,7 @@ export function resolveKbRoot(kbRoot: string, maxDepth = 3): KbSnapshot {
|
||||||
country: parsed.country,
|
country: parsed.country,
|
||||||
version: parsed.version,
|
version: parsed.version,
|
||||||
content: parsed.content,
|
content: parsed.content,
|
||||||
|
paths: { ...parsed.paths },
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* Register operator-subset knowledge pack skills (_skills dir) into DSH when KB is configured.
|
* Register operator-subset knowledge pack skills into DSH when KB is configured.
|
||||||
|
*
|
||||||
|
* Sources (same plane / toggles / loadSkillBundle rules):
|
||||||
|
* - `paths.skills` or default `_skills/` when `content.hasSkills`
|
||||||
|
* - `paths.localSkills` when present (missing field or empty/missing dir → skip)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readdir, stat } from 'node:fs/promises'
|
import { readdir, stat } from 'node:fs/promises'
|
||||||
|
|
@ -10,52 +14,55 @@ import { loadSkillBundle } from './group-skills.ts'
|
||||||
|
|
||||||
export type KbSkillPlane = 'preset' | 'public'
|
export type KbSkillPlane = 'preset' | 'public'
|
||||||
|
|
||||||
|
type SkillsRegister = (skill: {
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
content: string
|
||||||
|
path?: string
|
||||||
|
resourceBase?: { kind: 'directory'; path: string }
|
||||||
|
provider?: string
|
||||||
|
source: string
|
||||||
|
}) => () => void
|
||||||
|
|
||||||
/** True when MANIFEST says the package ships skill packs. */
|
/** True when MANIFEST says the package ships skill packs. */
|
||||||
export function kbPackSkillsEnabled(snapshot: KbSnapshot): boolean {
|
export function kbPackSkillsEnabled(snapshot: KbSnapshot): boolean {
|
||||||
return snapshot.status === 'configured' && snapshot.content.hasSkills === true
|
return snapshot.status === 'configured' && snapshot.content.hasSkills === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Shared pack root: MANIFEST `paths.skills` or legacy `_skills`. */
|
||||||
|
export function resolveKbSkillsRoot(snapshot: KbSnapshot): string {
|
||||||
|
const rel = snapshot.paths.skills?.trim() || '_skills'
|
||||||
|
return join(snapshot.realRoot, rel)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register every valid skill under realRoot/_skills.
|
* Absolute local-skills root from MANIFEST `paths.localSkills`, or null when absent.
|
||||||
* No-op when not configured / hasSkills false / directory missing.
|
|
||||||
* @returns disposer that unregisters all pack skills.
|
|
||||||
*/
|
*/
|
||||||
export async function registerKbPackSkills(
|
export function resolveKbLocalSkillsRoot(snapshot: KbSnapshot): string | null {
|
||||||
|
const rel = snapshot.paths.localSkills?.trim()
|
||||||
|
if (!rel) return null
|
||||||
|
return join(snapshot.realRoot, rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerSkillsUnderRoot(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
snapshot: KbSnapshot,
|
skillsApi: { register: SkillsRegister },
|
||||||
options: {
|
root: string,
|
||||||
enabled: boolean
|
provider: string,
|
||||||
providerLabel?: string
|
options: { logMissing: boolean; label: string },
|
||||||
},
|
): Promise<Array<() => void>> {
|
||||||
): Promise<() => void> {
|
|
||||||
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
|
||||||
return () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillsApi = (ctx as { skills?: { register: (skill: {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
content: string
|
|
||||||
path?: string
|
|
||||||
resourceBase?: { kind: 'directory'; path: string }
|
|
||||||
provider?: string
|
|
||||||
source: string
|
|
||||||
}) => () => void } }).skills
|
|
||||||
if (!skillsApi || typeof skillsApi.register !== 'function') {
|
|
||||||
return () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = join(snapshot.realRoot, '_skills')
|
|
||||||
let entries: string[]
|
let entries: string[]
|
||||||
try {
|
try {
|
||||||
entries = await readdir(root)
|
entries = await readdir(root)
|
||||||
} catch {
|
} catch {
|
||||||
ctx.logger.info('netxops: kb pack skills dir missing at %s — skip', root)
|
if (options.logMissing) {
|
||||||
return () => {}
|
ctx.logger.info('netxops: kb %s dir missing at %s — skip', options.label, root)
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
if (entries.length === 0) return []
|
||||||
|
|
||||||
const disposers: Array<() => void> = []
|
const disposers: Array<() => void> = []
|
||||||
const provider = options.providerLabel ?? 'netxops-kb-pack'
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(root, entry)
|
const full = join(root, entry)
|
||||||
let isDir = false
|
let isDir = false
|
||||||
|
|
@ -70,14 +77,15 @@ export async function registerKbPackSkills(
|
||||||
skill = await loadSkillBundle(full)
|
skill = await loadSkillBundle(full)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ctx.logger.warn(
|
ctx.logger.warn(
|
||||||
'netxops: kb pack skill load failed at %s: %s',
|
'netxops: kb %s skill load failed at %s: %s',
|
||||||
|
options.label,
|
||||||
full,
|
full,
|
||||||
error instanceof Error ? error.message : String(error),
|
error instanceof Error ? error.message : String(error),
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!skill) {
|
if (!skill) {
|
||||||
ctx.logger.warn('netxops: kb pack skill skipped (invalid SKILL.md): %s', full)
|
ctx.logger.warn('netxops: kb %s skill skipped (invalid SKILL.md): %s', options.label, full)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
disposers.push(skillsApi.register({
|
disposers.push(skillsApi.register({
|
||||||
|
|
@ -90,12 +98,55 @@ export async function registerKbPackSkills(
|
||||||
source: 'custom',
|
source: 'custom',
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
return disposers
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register every valid skill under paths.skills (or `_skills`) and paths.localSkills.
|
||||||
|
* No-op when not configured / hasSkills false.
|
||||||
|
* localSkills: field missing or directory empty/missing → skip (no error).
|
||||||
|
* @returns disposer that unregisters all pack skills.
|
||||||
|
*/
|
||||||
|
export async function registerKbPackSkills(
|
||||||
|
ctx: Context,
|
||||||
|
snapshot: KbSnapshot,
|
||||||
|
options: {
|
||||||
|
enabled: boolean
|
||||||
|
providerLabel?: string
|
||||||
|
},
|
||||||
|
): Promise<() => void> {
|
||||||
|
if (!options.enabled || !kbPackSkillsEnabled(snapshot)) {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillsApi = (ctx as { skills?: { register: SkillsRegister } }).skills
|
||||||
|
if (!skillsApi || typeof skillsApi.register !== 'function') {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = options.providerLabel ?? 'netxops-kb-pack'
|
||||||
|
const disposers: Array<() => void> = []
|
||||||
|
|
||||||
|
const skillsRoot = resolveKbSkillsRoot(snapshot)
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, skillsRoot, provider, {
|
||||||
|
logMissing: true,
|
||||||
|
label: 'pack',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const localRoot = resolveKbLocalSkillsRoot(snapshot)
|
||||||
|
if (localRoot) {
|
||||||
|
disposers.push(...await registerSkillsUnderRoot(ctx, skillsApi, localRoot, provider, {
|
||||||
|
logMissing: false,
|
||||||
|
label: 'local',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
if (disposers.length > 0) {
|
if (disposers.length > 0) {
|
||||||
ctx.logger.info(
|
ctx.logger.info(
|
||||||
'netxops: kb pack skills registered count=%s root=%s provider=%s',
|
'netxops: kb pack skills registered count=%s skillsRoot=%s localSkills=%s provider=%s',
|
||||||
disposers.length,
|
disposers.length,
|
||||||
root,
|
skillsRoot,
|
||||||
|
localRoot ?? '(none)',
|
||||||
provider,
|
provider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,12 @@ function store(): Store {
|
||||||
|
|
||||||
/** @returns the last published KB snapshot. */
|
/** @returns the last published KB snapshot. */
|
||||||
export function getKbContext(): KbSnapshot {
|
export function getKbContext(): KbSnapshot {
|
||||||
return { ...store().snapshot, content: { ...store().snapshot.content } }
|
const snap = store().snapshot
|
||||||
|
return {
|
||||||
|
...snap,
|
||||||
|
content: { ...snap.content },
|
||||||
|
paths: { ...snap.paths },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -51,6 +56,7 @@ export function publishKbContext(next: KbSnapshot): void {
|
||||||
state.snapshot = {
|
state.snapshot = {
|
||||||
...next,
|
...next,
|
||||||
content: { ...next.content },
|
content: { ...next.content },
|
||||||
|
paths: { ...next.paths },
|
||||||
}
|
}
|
||||||
for (const listener of state.listeners) listener()
|
for (const listener of state.listeners) listener()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import test from 'node:test'
|
||||||
import {
|
import {
|
||||||
findManifest,
|
findManifest,
|
||||||
parseManifest,
|
parseManifest,
|
||||||
|
parsePaths,
|
||||||
resolveKbRoot,
|
resolveKbRoot,
|
||||||
} from '../src/netx/kb-manifest.ts'
|
} from '../src/netx/kb-manifest.ts'
|
||||||
|
|
||||||
|
|
@ -190,5 +191,41 @@ test('contract content flags hasRegions etc. are accepted', () => {
|
||||||
assert.equal(snap.content.hasRegions, true)
|
assert.equal(snap.content.hasRegions, true)
|
||||||
assert.equal(snap.content.hasSkills, true)
|
assert.equal(snap.content.hasSkills, true)
|
||||||
assert.equal(snap.content.hasCommon, true)
|
assert.equal(snap.content.hasCommon, true)
|
||||||
|
assert.deepEqual(snap.paths, {})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('paths.skills / paths.localSkills parsed; blank skipped', () => {
|
||||||
|
const parsed = parseManifest(validManifest({
|
||||||
|
paths: {
|
||||||
|
skills: '_skills',
|
||||||
|
localSkills: 'regions/印尼-Indonesia/IOH/_local/local_skills',
|
||||||
|
rca: 'regions/印尼-Indonesia/IOH/00_有效RCA',
|
||||||
|
empty: ' ',
|
||||||
|
bad: 12,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
assert.equal(parsed.paths.skills, '_skills')
|
||||||
|
assert.equal(parsed.paths.localSkills, 'regions/印尼-Indonesia/IOH/_local/local_skills')
|
||||||
|
assert.equal(parsed.paths.rca, 'regions/印尼-Indonesia/IOH/00_有效RCA')
|
||||||
|
assert.equal(parsed.paths.empty, undefined)
|
||||||
|
assert.equal(parsed.paths.bad, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('paths missing → empty object; invalid type rejected', () => {
|
||||||
|
assert.deepEqual(parsePaths(undefined), {})
|
||||||
|
assert.deepEqual(parsePaths(null), {})
|
||||||
|
assert.throws(() => parsePaths([]), /paths must be an object/)
|
||||||
|
withTemp((root) => {
|
||||||
|
writeFileSync(join(root, 'MANIFEST.json'), validManifest({
|
||||||
|
paths: {
|
||||||
|
skills: '_skills',
|
||||||
|
localSkills: 'regions/ID/IOH/_local/local_skills',
|
||||||
|
},
|
||||||
|
}), 'utf8')
|
||||||
|
const snap = resolveKbRoot(root)
|
||||||
|
assert.equal(snap.status, 'configured')
|
||||||
|
assert.equal(snap.paths.skills, '_skills')
|
||||||
|
assert.equal(snap.paths.localSkills, 'regions/ID/IOH/_local/local_skills')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* KB pack skill loader from realRoot/_skills.
|
* KB pack skill loader from realRoot/_skills (+ optional paths.localSkills).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
|
|
@ -8,7 +8,12 @@ import { tmpdir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
import { kbPackSkillsEnabled, registerKbPackSkills } from '../src/netx/kb-pack-skills.ts'
|
import {
|
||||||
|
kbPackSkillsEnabled,
|
||||||
|
registerKbPackSkills,
|
||||||
|
resolveKbLocalSkillsRoot,
|
||||||
|
resolveKbSkillsRoot,
|
||||||
|
} from '../src/netx/kb-pack-skills.ts'
|
||||||
|
|
||||||
async function withTemp(run) {
|
async function withTemp(run) {
|
||||||
const root = mkdtempSync(join(tmpdir(), 'netxops-kb-pack-'))
|
const root = mkdtempSync(join(tmpdir(), 'netxops-kb-pack-'))
|
||||||
|
|
@ -19,7 +24,7 @@ async function withTemp(run) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function configuredSnap(realRoot, content = {}) {
|
function configuredSnap(realRoot, content = {}, paths = {}) {
|
||||||
return {
|
return {
|
||||||
status: 'configured',
|
status: 'configured',
|
||||||
realRoot,
|
realRoot,
|
||||||
|
|
@ -34,6 +39,7 @@ function configuredSnap(realRoot, content = {}) {
|
||||||
hasSkills: true,
|
hasSkills: true,
|
||||||
...content,
|
...content,
|
||||||
},
|
},
|
||||||
|
paths: { ...paths },
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,3 +99,108 @@ test('registerKbPackSkills no-op when disabled or hasSkills false', async () =>
|
||||||
await registerKbPackSkills(ctx, configuredSnap('/nope', { hasSkills: false }), { enabled: true })
|
await registerKbPackSkills(ctx, configuredSnap('/nope', { hasSkills: false }), { enabled: true })
|
||||||
assert.equal(registered.length, 0)
|
assert.equal(registered.length, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('resolveKbSkillsRoot uses paths.skills or defaults to _skills', () => {
|
||||||
|
const snap = configuredSnap('/kb')
|
||||||
|
assert.equal(resolveKbSkillsRoot(snap), join('/kb', '_skills'))
|
||||||
|
assert.equal(
|
||||||
|
resolveKbSkillsRoot(configuredSnap('/kb', {}, { skills: 'pack/skills' })),
|
||||||
|
join('/kb', 'pack/skills'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolveKbLocalSkillsRoot null when missing; joins when set', () => {
|
||||||
|
assert.equal(resolveKbLocalSkillsRoot(configuredSnap('/kb')), null)
|
||||||
|
assert.equal(
|
||||||
|
resolveKbLocalSkillsRoot(configuredSnap('/kb', {}, {
|
||||||
|
localSkills: 'regions/ID/IOH/_local/local_skills',
|
||||||
|
})),
|
||||||
|
join('/kb', 'regions/ID/IOH/_local/local_skills'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('registerKbPackSkills also loads paths.localSkills/*/SKILL.md', async () => {
|
||||||
|
await withTemp(async (root) => {
|
||||||
|
const packDir = join(root, '_skills', 'kb-troubleshoot')
|
||||||
|
mkdirSync(packDir, { recursive: true })
|
||||||
|
writeFileSync(join(packDir, 'SKILL.md'), `---
|
||||||
|
name: kb-troubleshoot
|
||||||
|
description: KB triage playbook
|
||||||
|
---
|
||||||
|
|
||||||
|
# kb-troubleshoot
|
||||||
|
`, 'utf8')
|
||||||
|
|
||||||
|
const localRel = join('regions', 'ID', 'IOH', '_local', 'local_skills')
|
||||||
|
const localDir = join(root, localRel, 'ioh-local-rca')
|
||||||
|
mkdirSync(localDir, { recursive: true })
|
||||||
|
writeFileSync(join(localDir, 'SKILL.md'), `---
|
||||||
|
name: ioh-local-rca
|
||||||
|
description: IOH local RCA playbook
|
||||||
|
---
|
||||||
|
|
||||||
|
# ioh-local-rca
|
||||||
|
`, 'utf8')
|
||||||
|
|
||||||
|
const registered = []
|
||||||
|
const ctx = {
|
||||||
|
logger: { info() {}, warn() {} },
|
||||||
|
skills: {
|
||||||
|
register(skill) {
|
||||||
|
registered.push(skill)
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await registerKbPackSkills(ctx, configuredSnap(root, {}, {
|
||||||
|
skills: '_skills',
|
||||||
|
localSkills: localRel.replace(/\\/g, '/'),
|
||||||
|
}), { enabled: true })
|
||||||
|
|
||||||
|
const names = registered.map((s) => s.name).sort()
|
||||||
|
assert.deepEqual(names, ['ioh-local-rca', 'kb-troubleshoot'])
|
||||||
|
assert.ok(registered.every((s) => s.provider === 'netxops-kb-pack'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('registerKbPackSkills skips missing or empty localSkills', async () => {
|
||||||
|
await withTemp(async (root) => {
|
||||||
|
const skillDir = join(root, '_skills', 'kb-troubleshoot')
|
||||||
|
mkdirSync(skillDir, { recursive: true })
|
||||||
|
writeFileSync(join(skillDir, 'SKILL.md'), `---
|
||||||
|
name: kb-troubleshoot
|
||||||
|
description: KB triage playbook
|
||||||
|
---
|
||||||
|
|
||||||
|
# kb-troubleshoot
|
||||||
|
`, 'utf8')
|
||||||
|
|
||||||
|
const emptyLocal = join(root, '_local', 'local_skills')
|
||||||
|
mkdirSync(emptyLocal, { recursive: true })
|
||||||
|
|
||||||
|
const registered = []
|
||||||
|
const ctx = {
|
||||||
|
logger: { info() {}, warn() {} },
|
||||||
|
skills: {
|
||||||
|
register(skill) {
|
||||||
|
registered.push(skill)
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await registerKbPackSkills(ctx, configuredSnap(root, {}, {
|
||||||
|
localSkills: '_local/local_skills',
|
||||||
|
}), { enabled: true })
|
||||||
|
assert.equal(registered.length, 1)
|
||||||
|
assert.equal(registered[0].name, 'kb-troubleshoot')
|
||||||
|
|
||||||
|
registered.length = 0
|
||||||
|
await registerKbPackSkills(ctx, configuredSnap(root, {}, {
|
||||||
|
localSkills: '_local/does-not-exist',
|
||||||
|
}), { enabled: true })
|
||||||
|
assert.equal(registered.length, 1)
|
||||||
|
assert.equal(registered[0].name, 'kb-troubleshoot')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -32,11 +32,14 @@ test('publishKbContext updates store', () => {
|
||||||
hasCommon: false,
|
hasCommon: false,
|
||||||
hasSkills: false,
|
hasSkills: false,
|
||||||
},
|
},
|
||||||
|
paths: { skills: '_skills', localSkills: 'regions/ID/IOH/_local/local_skills' },
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
})
|
})
|
||||||
const snap = getKbContext()
|
const snap = getKbContext()
|
||||||
assert.equal(snap.status, 'configured')
|
assert.equal(snap.status, 'configured')
|
||||||
assert.equal(snap.operatorName, 'IOH')
|
assert.equal(snap.operatorName, 'IOH')
|
||||||
|
assert.equal(snap.paths.skills, '_skills')
|
||||||
|
assert.equal(snap.paths.localSkills, 'regions/ID/IOH/_local/local_skills')
|
||||||
applyKbEnv(snap)
|
applyKbEnv(snap)
|
||||||
assert.equal(process.env.KB_OPERATOR, 'IOH')
|
assert.equal(process.env.KB_OPERATOR, 'IOH')
|
||||||
assert.equal(process.env.KB_COUNTRY, 'ID')
|
assert.equal(process.env.KB_COUNTRY, 'ID')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue