mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 04:00:46 +08:00
Add bizMonitor host tools for cutover analysis via netx REST (0.1.40).
Expose getBizMonitor* / getBizCollect* on the Ops preset with skill netx-biz-monitor; keep tooling in netxops instead of netx-mcp. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f7801db296
commit
0934e10409
23 changed files with 1022 additions and 62 deletions
15
src/agent-tools-biz-monitor.ts
Normal file
15
src/agent-tools-biz-monitor.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Forced bizMonitor-group tools + skill (cutover / biz_state read analysis).
|
||||
*
|
||||
* @module dsh-netxops/tools-biz-monitor
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { applyGroupToolsPlugin } from './netx/group-tools-plugin.ts'
|
||||
|
||||
export const name = 'netxops-tools-biz-monitor'
|
||||
export const inject = ['tools']
|
||||
|
||||
export function apply(ctx: Context): void {
|
||||
applyGroupToolsPlugin(ctx, { name, mode: 'forced', only: ['bizMonitor'] })
|
||||
}
|
||||
|
|
@ -564,6 +564,20 @@ export function NetxopsCard(props: NetxopsCardProps) {
|
|||
props.edit('groupTopologyPublic', checked ? 'true' : 'false')
|
||||
}}
|
||||
/>
|
||||
<CapabilityGroupBlock
|
||||
title={t('groupBizMonitor')}
|
||||
inPresetLabel={t('groupInPreset')}
|
||||
publicLabel={t('groupPublic')}
|
||||
inPreset={state.groupBizMonitorInPreset}
|
||||
published={state.groupBizMonitorPublic}
|
||||
disabled={disabled}
|
||||
onEditInPreset={(checked) => {
|
||||
props.edit('groupBizMonitorInPreset', checked ? 'true' : 'false')
|
||||
}}
|
||||
onEditPublic={(checked) => {
|
||||
props.edit('groupBizMonitorPublic', checked ? 'true' : 'false')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="dsh-nx-settings-card">
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ export interface NetxopsSettings {
|
|||
groupOpsPublic?: boolean
|
||||
groupTopologyInPreset?: boolean
|
||||
groupTopologyPublic?: boolean
|
||||
groupBizMonitorInPreset?: boolean
|
||||
groupBizMonitorPublic?: boolean
|
||||
nmsProvider?: string
|
||||
/** Operator-subset knowledge package root (Host-local path). */
|
||||
kbRoot?: string
|
||||
|
|
@ -82,6 +84,8 @@ export interface NetxopsCardState extends CardShell {
|
|||
groupOpsPublic: CardFieldState
|
||||
groupTopologyInPreset: CardFieldState
|
||||
groupTopologyPublic: CardFieldState
|
||||
groupBizMonitorInPreset: CardFieldState
|
||||
groupBizMonitorPublic: CardFieldState
|
||||
nmsProvider: CardFieldState
|
||||
kbRoot: CardFieldState
|
||||
groupKbInPreset: CardFieldState
|
||||
|
|
@ -187,6 +191,8 @@ export class NetxopsCardController {
|
|||
booleanField('groupOpsPublic'),
|
||||
booleanField('groupTopologyInPreset'),
|
||||
booleanField('groupTopologyPublic'),
|
||||
booleanFieldPersistFalse('groupBizMonitorInPreset'),
|
||||
booleanField('groupBizMonitorPublic'),
|
||||
booleanFieldPersistFalse('groupKbInPreset'),
|
||||
booleanField('groupKbPublic'),
|
||||
booleanField('alarmPushEnabled'),
|
||||
|
|
@ -599,6 +605,8 @@ export class NetxopsCardController {
|
|||
groupOpsPublic: this.form.field('groupOpsPublic'),
|
||||
groupTopologyInPreset: this.form.field('groupTopologyInPreset'),
|
||||
groupTopologyPublic: this.form.field('groupTopologyPublic'),
|
||||
groupBizMonitorInPreset: this.form.field('groupBizMonitorInPreset'),
|
||||
groupBizMonitorPublic: this.form.field('groupBizMonitorPublic'),
|
||||
groupKbInPreset: this.form.field('groupKbInPreset'),
|
||||
groupKbPublic: this.form.field('groupKbPublic'),
|
||||
alarmPushEnabled: this.form.field('alarmPushEnabled'),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export type NetxopsLocaleKey =
|
|||
| 'nmsProviderHint'
|
||||
| 'groupOps'
|
||||
| 'groupTopology'
|
||||
| 'groupBizMonitor'
|
||||
| 'groupKb'
|
||||
| 'groupInPreset'
|
||||
| 'groupPublic'
|
||||
|
|
@ -98,6 +99,7 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
|||
nmsProviderHint: 'zte-ume',
|
||||
groupOps: 'ops',
|
||||
groupTopology: 'topology',
|
||||
groupBizMonitor: 'bizMonitor',
|
||||
groupKb: 'Knowledge skills',
|
||||
groupInPreset: 'In Netx Ops preset',
|
||||
groupPublic: 'Publish to other presets',
|
||||
|
|
@ -176,6 +178,7 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
|||
nmsProviderHint: 'zte-ume',
|
||||
groupOps: 'ops',
|
||||
groupTopology: 'topology',
|
||||
groupBizMonitor: 'bizMonitor',
|
||||
groupKb: '知识库技能',
|
||||
groupInPreset: '在 Netx Ops 预设中启用',
|
||||
groupPublic: '对其他预设公开',
|
||||
|
|
|
|||
|
|
@ -152,6 +152,10 @@ export interface Config {
|
|||
groupTopologyInPreset: boolean
|
||||
/** Publish topology tools/skills to other presets (default off). */
|
||||
groupTopologyPublic: boolean
|
||||
/** biz-monitor (cutover / biz_state read) tools in the Netx Ops preset (default on). */
|
||||
groupBizMonitorInPreset: boolean
|
||||
/** Publish biz-monitor tools/skills to other presets (default off). */
|
||||
groupBizMonitorPublic: boolean
|
||||
/**
|
||||
* Absolute (or Host-local) path to an operator-subset knowledge package.
|
||||
* Empty = unconfigured. Plugin locates `MANIFEST.json` (≤3 levels) and
|
||||
|
|
@ -183,6 +187,8 @@ export const Config: z<Config> = z.object({
|
|||
groupOpsPublic: z.boolean().default(false),
|
||||
groupTopologyInPreset: z.boolean().default(false),
|
||||
groupTopologyPublic: z.boolean().default(false),
|
||||
groupBizMonitorInPreset: z.boolean().default(true),
|
||||
groupBizMonitorPublic: z.boolean().default(false),
|
||||
kbRoot: z.string().default(''),
|
||||
groupKbInPreset: z.boolean().default(true),
|
||||
groupKbPublic: z.boolean().default(false),
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@
|
|||
*
|
||||
* - `ops` → skill `netx-ops` — NMS alarms/inventory/SQL + managed CLI + findTopologyPaths
|
||||
* - `topology` → skill `netx-topology` — canvas / fabric / dual_unit / layout
|
||||
* - `bizMonitor` → skill `netx-biz-monitor` — cutover / biz_state read + overnight analysis
|
||||
*/
|
||||
|
||||
/** Stable group ids used in settings, skill dirs, and registration filters. */
|
||||
export type NetxCapabilityGroupId = 'ops' | 'topology'
|
||||
export type NetxCapabilityGroupId = 'ops' | 'topology' | 'bizMonitor'
|
||||
|
||||
/** Per-group exposure knobs. */
|
||||
export interface NetxGroupExposure {
|
||||
|
|
@ -27,14 +28,17 @@ export interface NetxCapabilityGroupSettingsFields {
|
|||
groupOpsPublic: boolean
|
||||
groupTopologyInPreset: boolean
|
||||
groupTopologyPublic: boolean
|
||||
groupBizMonitorInPreset: boolean
|
||||
groupBizMonitorPublic: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Default: ops in Ops preset; topology and all public off.
|
||||
* Default: ops in Ops preset; topology / bizMonitor and all public off.
|
||||
*/
|
||||
export const DEFAULT_CAPABILITY_GROUPS: NetxCapabilityGroups = Object.freeze({
|
||||
ops: Object.freeze({ inPreset: true, public: false }),
|
||||
topology: Object.freeze({ inPreset: false, public: false }),
|
||||
bizMonitor: Object.freeze({ inPreset: true, public: false }),
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -76,17 +80,27 @@ export const TOOLS_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, readonly str
|
|||
'netx__analyzeTopologyViewLayout',
|
||||
'netx__sinkTopologyDualUnits',
|
||||
]),
|
||||
bizMonitor: Object.freeze([
|
||||
'netx__getBizMonitorContext',
|
||||
'netx__getBizMonitorBoard',
|
||||
'netx__listBizMonitorReds',
|
||||
'netx__getBizMonitorDiffs',
|
||||
'netx__getBizCollectBatch',
|
||||
'netx__getBizCollectCommandRaw',
|
||||
]),
|
||||
})
|
||||
|
||||
/** Skill directory name under skills root / `presets/netxops/skills/<group>/`. */
|
||||
export const SKILL_DIR_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, string>> = Object.freeze({
|
||||
ops: 'ops',
|
||||
topology: 'topology',
|
||||
bizMonitor: 'biz-monitor',
|
||||
})
|
||||
|
||||
export const CAPABILITY_GROUP_IDS: readonly NetxCapabilityGroupId[] = Object.freeze([
|
||||
'ops',
|
||||
'topology',
|
||||
'bizMonitor',
|
||||
])
|
||||
|
||||
/**
|
||||
|
|
@ -126,6 +140,11 @@ export function capabilityGroupsFromSettings(
|
|||
}
|
||||
}
|
||||
|
||||
let bizMonitorInPreset = true
|
||||
if (src.groupBizMonitorInPreset !== undefined) {
|
||||
bizMonitorInPreset = src.groupBizMonitorInPreset !== false
|
||||
}
|
||||
|
||||
return {
|
||||
ops: {
|
||||
inPreset: opsInPreset,
|
||||
|
|
@ -140,6 +159,10 @@ export function capabilityGroupsFromSettings(
|
|||
public: src.groupTopologyPublic === true
|
||||
|| src.groupTopologyLayoutPublic === true,
|
||||
},
|
||||
bizMonitor: {
|
||||
inPreset: bizMonitorInPreset,
|
||||
public: src.groupBizMonitorPublic === true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -177,9 +200,9 @@ export function groupsForced(
|
|||
* Tool name allow-list for the given groups.
|
||||
*/
|
||||
export function toolNamesForGroups(groupIds: readonly NetxCapabilityGroupId[]): Set<string> {
|
||||
const names = new Set<string>()
|
||||
const out = new Set<string>()
|
||||
for (const id of groupIds) {
|
||||
for (const tool of TOOLS_BY_GROUP[id]) names.add(tool)
|
||||
for (const name of TOOLS_BY_GROUP[id] ?? []) out.add(name)
|
||||
}
|
||||
return names
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -330,3 +330,101 @@ export async function findTopologyPaths(client: NetxClient, args: NetxJson, sign
|
|||
else body.to_managed_ne_id = toMid
|
||||
return client.post('/v1/topology/fabric/paths', body, signal, 30_000)
|
||||
}
|
||||
|
||||
// ── bizMonitor group (cutover / biz_state read) ─────────────────────────────
|
||||
|
||||
/** Fat cutover / biz_state definition bundle (templates, mapping, tasks). */
|
||||
export async function getBizMonitorContext(
|
||||
client: NetxClient,
|
||||
args: NetxJson,
|
||||
signal?: AbortSignal,
|
||||
): Promise<NetxJson> {
|
||||
const projectId = str(args, 'project_id').trim()
|
||||
const taskId = str(args, 'task_id').trim()
|
||||
if (!projectId && !taskId) {
|
||||
return { ok: false, error: 'project_id_or_task_id_required' }
|
||||
}
|
||||
const params: Record<string, string | number | boolean> = {}
|
||||
if (projectId) params.project_id = projectId
|
||||
if (taskId) params.task_id = taskId
|
||||
return client.get('/v1/biz-migration/monitor-context', params, signal)
|
||||
}
|
||||
|
||||
/** Cutover batch board + latest evaluate run summary. */
|
||||
export async function getBizMonitorBoard(
|
||||
client: NetxClient,
|
||||
args: NetxJson,
|
||||
signal?: AbortSignal,
|
||||
): Promise<NetxJson> {
|
||||
const batchId = str(args, 'batch_id').trim()
|
||||
if (!batchId) return { ok: false, error: 'batch_id_required' }
|
||||
const params: Record<string, string | number | boolean> = {}
|
||||
const runId = str(args, 'run_id').trim()
|
||||
if (runId) params.run_id = runId
|
||||
return client.get(`/v1/biz-migration/batches/${encodeURIComponent(batchId)}/board`, params, signal)
|
||||
}
|
||||
|
||||
/** Red tickets with evidence (raw A/B + show command). */
|
||||
export async function listBizMonitorReds(
|
||||
client: NetxClient,
|
||||
args: NetxJson,
|
||||
signal?: AbortSignal,
|
||||
): Promise<NetxJson> {
|
||||
const projectId = str(args, 'project_id').trim()
|
||||
if (!projectId) return { ok: false, error: 'project_id_required' }
|
||||
const params: Record<string, string | number | boolean> = {}
|
||||
putStr(params, args, ['status'])
|
||||
const limit = num(args, 'limit')
|
||||
if (limit !== undefined) params.limit = clampInt(limit, 200, 1, 500)
|
||||
return client.get(
|
||||
`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/red-tickets`,
|
||||
params,
|
||||
signal,
|
||||
)
|
||||
}
|
||||
|
||||
/** Paged evaluate diffs (old_key/new_key = device-raw A/B). */
|
||||
export async function getBizMonitorDiffs(
|
||||
client: NetxClient,
|
||||
args: NetxJson,
|
||||
signal?: AbortSignal,
|
||||
): Promise<NetxJson> {
|
||||
const runId = str(args, 'run_id').trim()
|
||||
if (!runId) return { ok: false, error: 'run_id_required' }
|
||||
const params: Record<string, string | number | boolean> = {}
|
||||
putStr(params, args, ['metric_id', 'sheet_id', 'verdict', 'color', 'q'])
|
||||
const limit = num(args, 'limit')
|
||||
if (limit !== undefined) params.limit = clampInt(limit, 200, 1, 500)
|
||||
const offset = num(args, 'offset')
|
||||
if (offset !== undefined) params.offset = clampInt(offset, 0, 0, Number.MAX_SAFE_INTEGER)
|
||||
return client.get(`/v1/biz-migration/runs/${encodeURIComponent(runId)}/diffs`, params, signal)
|
||||
}
|
||||
|
||||
/** One biz_state collect batch (commands + metrics). */
|
||||
export async function getBizCollectBatch(
|
||||
client: NetxClient,
|
||||
args: NetxJson,
|
||||
signal?: AbortSignal,
|
||||
): Promise<NetxJson> {
|
||||
const batchId = str(args, 'batch_id').trim()
|
||||
if (!batchId) return { ok: false, error: 'batch_id_required' }
|
||||
return client.get(`/v1/biz-state/batches/${encodeURIComponent(batchId)}`, {}, signal)
|
||||
}
|
||||
|
||||
/** Full CLI raw_text for one collect command. */
|
||||
export async function getBizCollectCommandRaw(
|
||||
client: NetxClient,
|
||||
args: NetxJson,
|
||||
signal?: AbortSignal,
|
||||
): Promise<NetxJson> {
|
||||
const batchId = str(args, 'batch_id').trim()
|
||||
const commandId = str(args, 'command_id').trim()
|
||||
if (!batchId || !commandId) {
|
||||
return { ok: false, error: 'batch_id_and_command_id_required' }
|
||||
}
|
||||
return client.get(
|
||||
`/v1/biz-state/batches/${encodeURIComponent(batchId)}/commands/${encodeURIComponent(commandId)}`,
|
||||
{},
|
||||
signal,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -316,6 +316,67 @@ export function registerNetxTools(
|
|||
},
|
||||
H.findTopologyPaths, getClient, t,
|
||||
),
|
||||
// ── bizMonitor group (cutover / biz_state read via netx REST) ─────────
|
||||
tool(
|
||||
'netx__getBizMonitorContext',
|
||||
'Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id. Display keys are device-raw A/B.',
|
||||
{
|
||||
project_id: str('Cutover / biz-migration project id'),
|
||||
task_id: str('biz_state task id (optional if project_id set)'),
|
||||
},
|
||||
H.getBizMonitorContext, getClient, t,
|
||||
),
|
||||
tool(
|
||||
'netx__getBizMonitorBoard',
|
||||
'Cutover batch board: project/batch status, evaluate run summary, sheet cards, verdict counts, metric_batches.',
|
||||
{
|
||||
batch_id: { type: 'string' as const, required: true as const },
|
||||
run_id: str('Optional evaluate run id'),
|
||||
},
|
||||
H.getBizMonitorBoard, getClient, t,
|
||||
),
|
||||
tool(
|
||||
'netx__listBizMonitorReds',
|
||||
'Open/carried red tickets. Each item has old_key/new_key (raw A/B) and evidence (device, collect time, show command, iface raw→norm→map).',
|
||||
{
|
||||
project_id: { type: 'string' as const, required: true as const },
|
||||
status: str('open|carried|resolved or empty=all'),
|
||||
limit: num(),
|
||||
},
|
||||
H.listBizMonitorReds, getClient, t,
|
||||
),
|
||||
tool(
|
||||
'netx__getBizMonitorDiffs',
|
||||
'Paged evaluate diffs. Rows expose old_key/new_key (raw A/B), match_* keys, status, evidence with show commands.',
|
||||
{
|
||||
run_id: { type: 'string' as const, required: true as const },
|
||||
metric_id: str(),
|
||||
sheet_id: str(),
|
||||
verdict: str(),
|
||||
color: str('e.g. red'),
|
||||
q: str(),
|
||||
limit: num(),
|
||||
offset: num(),
|
||||
},
|
||||
H.getBizMonitorDiffs, getClient, t,
|
||||
),
|
||||
tool(
|
||||
'netx__getBizCollectBatch',
|
||||
'One biz_state collect batch: commands (raw_command, parse_status, preview), parsed metrics.',
|
||||
{
|
||||
batch_id: { type: 'string' as const, required: true as const },
|
||||
},
|
||||
H.getBizCollectBatch, getClient, t,
|
||||
),
|
||||
tool(
|
||||
'netx__getBizCollectCommandRaw',
|
||||
'Full CLI raw_text for one collect command inside a batch (deep dive).',
|
||||
{
|
||||
batch_id: { type: 'string' as const, required: true as const },
|
||||
command_id: { type: 'string' as const, required: true as const },
|
||||
},
|
||||
H.getBizCollectCommandRaw, getClient, t,
|
||||
),
|
||||
// ── topology group (netx-topology canvas / fabric) ─────────────────────
|
||||
tool(
|
||||
'netx__getTopologyTree',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue