mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 23:10:49 +08:00
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:
parent
fcdd11ac59
commit
2caa3c98d7
26 changed files with 1122 additions and 106 deletions
|
|
@ -21,6 +21,8 @@ interface ParsedSkill {
|
|||
directory: string
|
||||
}
|
||||
|
||||
export type { ParsedSkill }
|
||||
|
||||
/**
|
||||
* Package `presets/netxops/skills` root, or a shared netx skills checkout.
|
||||
*
|
||||
|
|
@ -95,7 +97,10 @@ function stripQuotes(value: string): string {
|
|||
return value
|
||||
}
|
||||
|
||||
async function loadSkillBundle(dir: string): Promise<ParsedSkill | null> {
|
||||
/**
|
||||
* Load one skill directory (`SKILL.md` + frontmatter). Exported for KB pack loader.
|
||||
*/
|
||||
export async function loadSkillBundle(dir: string): Promise<ParsedSkill | null> {
|
||||
const skillPath = join(dir, 'SKILL.md')
|
||||
let raw: string
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from './capability-groups.ts'
|
||||
import { registerGroupSkills } from './group-skills.ts'
|
||||
import { registerKbContextSkill } from './kb-context-skill.ts'
|
||||
import { registerKbPackSkills } from './kb-pack-skills.ts'
|
||||
import { getKbContext, watchKbContext } from './kb-runtime.ts'
|
||||
import { getNetxConnection, watchNetxConnection } from './runtime.ts'
|
||||
import { registerNetxTools } from './tools.ts'
|
||||
|
|
@ -75,6 +76,8 @@ export function applyGroupToolsPlugin(ctx: Context, options: GroupToolsPluginOpt
|
|||
|
||||
ctx.inject(['skills'], (skillsCtx) => {
|
||||
let unregisterKbSkill: (() => void) | undefined
|
||||
let unregisterKbPack: (() => void) | undefined
|
||||
let packGeneration = 0
|
||||
const remountSkills = (): void => {
|
||||
const gen = ++skillGeneration
|
||||
unregisterSkills?.()
|
||||
|
|
@ -96,19 +99,48 @@ export function applyGroupToolsPlugin(ctx: Context, options: GroupToolsPluginOpt
|
|||
unregisterKbSkill = undefined
|
||||
unregisterKbSkill = registerKbContextSkill(skillsCtx, getKbContext())
|
||||
}
|
||||
const remountKbPack = (): void => {
|
||||
const gen = ++packGeneration
|
||||
unregisterKbPack?.()
|
||||
unregisterKbPack = undefined
|
||||
const connection = getNetxConnection()
|
||||
const enabled = connection?.groupKbInPreset !== false
|
||||
void 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()
|
||||
const stopSkillWatch = watchNetxConnection(() => { remountSkills() })
|
||||
const stopKbWatch = watchKbContext(() => { 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`)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ function skillBody(snapshot: KbSnapshot): { description: string; content: string
|
|||
`| 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('\n'),
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +51,7 @@ function skillBody(snapshot: KbSnapshot): { description: string; content: string
|
|||
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).',
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@
|
|||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
|
||||
/** Known content flags; unknown keys are preserved when boolean. */
|
||||
/** Known content flags (MANIFEST contract v1.0); unknown boolean keys may be preserved. */
|
||||
export interface KbContentFlags {
|
||||
regions: boolean
|
||||
theory: boolean
|
||||
packet: boolean
|
||||
skills: boolean
|
||||
hasRegions: boolean
|
||||
hasTheory: boolean
|
||||
hasPacket: boolean
|
||||
hasCommon: boolean
|
||||
hasSkills: boolean
|
||||
[key: string]: boolean
|
||||
}
|
||||
|
||||
|
|
@ -37,10 +38,20 @@ export interface KbSnapshot {
|
|||
}
|
||||
|
||||
const EMPTY_CONTENT: KbContentFlags = {
|
||||
regions: false,
|
||||
theory: false,
|
||||
packet: false,
|
||||
skills: false,
|
||||
hasRegions: false,
|
||||
hasTheory: false,
|
||||
hasPacket: false,
|
||||
hasCommon: false,
|
||||
hasSkills: false,
|
||||
}
|
||||
|
||||
/** Legacy short keys → contract `has*` (one-release compat). */
|
||||
const LEGACY_CONTENT_KEY: Record<string, keyof KbContentFlags> = {
|
||||
regions: 'hasRegions',
|
||||
theory: 'hasTheory',
|
||||
packet: 'hasPacket',
|
||||
skills: 'hasSkills',
|
||||
common: 'hasCommon',
|
||||
}
|
||||
|
||||
/** Empty / unconfigured snapshot (kbRoot blank). */
|
||||
|
|
@ -165,8 +176,17 @@ function parseContent(raw: unknown): KbContentFlags {
|
|||
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
throw new Error('MANIFEST content must be an object')
|
||||
}
|
||||
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
||||
out[key] = value === true
|
||||
const row = raw as Record<string, unknown>
|
||||
const present = new Set(Object.keys(row))
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
const on = value === true
|
||||
const mapped = LEGACY_CONTENT_KEY[key]
|
||||
if (mapped !== undefined) {
|
||||
// Short keys only fill has* when the contract key is absent (no dual truth).
|
||||
if (on && !present.has(mapped)) out[mapped] = true
|
||||
continue
|
||||
}
|
||||
out[key] = on
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
106
src/netx/kb-pack-skills.ts
Normal file
106
src/netx/kb-pack-skills.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* Register operator-subset knowledge pack skills (_skills dir) into DSH when KB is configured.
|
||||
*/
|
||||
|
||||
import { readdir, stat } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { KbSnapshot } from './kb-manifest.ts'
|
||||
import { loadSkillBundle } from './group-skills.ts'
|
||||
|
||||
export type KbSkillPlane = 'preset' | 'public'
|
||||
|
||||
/** True when MANIFEST says the package ships skill packs. */
|
||||
export function kbPackSkillsEnabled(snapshot: KbSnapshot): boolean {
|
||||
return snapshot.status === 'configured' && snapshot.content.hasSkills === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Register every valid skill under realRoot/_skills.
|
||||
* No-op when not configured / hasSkills false / directory missing.
|
||||
* @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: (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[]
|
||||
try {
|
||||
entries = await readdir(root)
|
||||
} catch {
|
||||
ctx.logger.info('netxops: kb pack skills dir missing at %s — skip', root)
|
||||
return () => {}
|
||||
}
|
||||
|
||||
const disposers: Array<() => void> = []
|
||||
const provider = options.providerLabel ?? 'netxops-kb-pack'
|
||||
for (const entry of entries) {
|
||||
const full = join(root, entry)
|
||||
let isDir = false
|
||||
try {
|
||||
isDir = (await stat(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()
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,13 @@ export interface NetxToolConnection {
|
|||
toolCallTimeoutMs: number
|
||||
/** Capability-group exposure; omitted → package defaults. */
|
||||
groups?: NetxCapabilityGroups
|
||||
/**
|
||||
* Mount operator-subset `_skills/kb-*` into the Netx Ops preset (default true).
|
||||
* Gated also by KB configured + content.hasSkills.
|
||||
*/
|
||||
groupKbInPreset?: boolean
|
||||
/** Mount kb pack skills on the host public skill layer (default false). */
|
||||
groupKbPublic?: boolean
|
||||
}
|
||||
|
||||
type Handler = (client: NetxClient, args: NetxJson, signal?: AbortSignal) => Promise<NetxJson>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue