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:
oliver 2026-09-19 22:48:42 +08:00
parent f7801db296
commit 0934e10409
23 changed files with 1022 additions and 62 deletions

View 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'] })
}

View file

@ -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">

View file

@ -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'),

View file

@ -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: '对其他预设公开',

View file

@ -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),

View file

@ -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
}

View file

@ -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,
)
}

View file

@ -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',