Merge nms+common into ops so login questions load execManagedNe.

One capability group now ships netx-ops (alarms + managed CLI + paths); keep tools-nms/common as aliases.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-06 02:11:02 +08:00
parent 6e591be2b5
commit 63f88bdf6c
33 changed files with 2133 additions and 446 deletions

View file

@ -1,4 +1,4 @@
# netxops — Netx Ops for DeepSeek Harness
# netxops — Netx Ops for DeepSeek Harness
Public **DeepSeek Harness** package: native `netx__*` REST tools + Plugins card + **agent preset (skills included)**.
@ -28,9 +28,9 @@ Also need: a reachable **netx API** (no local `pip install netx_mcp`). Details:
| In the plugin | Outside (data / runtime) |
|---------------|---------------------------|
| `netx__*` REST tools (groups: nms / common / topology; one group ↔ one skill; optional host publish + `tools-*`) + settings + credentials | netx HTTP API (URL + token) |
| `netx__*` REST tools (groups: ops / topology; one group ↔ one skill; optional host publish + `tools-ops|topology`) + settings + credentials | netx HTTP API (URL + token) |
| Optional key-alarm push (WSS client → sticky DSH session) | netx `/v1/integrations/dsh-alarm/ws` hub |
| Persona + skills (`netx-nms` / `netx-common` / `netx-topology`) | |
| Persona + skills (`netx-ops` / `netx-topology`) | |
| Agent preset auto-install to `~/.dsh/.agent-presets` | |
MCP (`python -m netx_mcp`) remains available for OpenClaw / other hosts — not required for DSH.

View file

@ -1,7 +1,7 @@
# Host-plane Netx Ops bridge: settings + credentials → connection publish.
# Ops-scoped `netx__*` tools mount from the agent preset (`dsh-netxops/tools`).
# Capability groups: nms + common in preset; topology / public off.
# Rule: one group ↔ one skill (netx-nms / netx-common / netx-topology).
# Capability groups: ops in preset; topology / public off.
# Rule: one group ↔ one skill (netx-ops / netx-topology).
#
# After `dsh plugin add` / link, open Settings → Plugins to edit apiUrl / lang / groups
# (settings card client half; see docs/INSTALL.md). Token: credential NETX_API_TOKEN.
@ -21,9 +21,7 @@
alarmDeliverIm: false
imBotId: ''
imTargetId: ''
groupNmsInPreset: true
groupNmsPublic: false
groupCommonInPreset: true
groupCommonPublic: false
groupOpsInPreset: true
groupOpsPublic: false
groupTopologyInPreset: false
groupTopologyPublic: false

View file

@ -1,4 +1,4 @@
# Install Netx Ops on DeepSeek Harness
# Install Netx Ops on DeepSeek Harness
## What `dsh plugin add` gives you
@ -6,7 +6,7 @@ One install wires **all of**:
| Piece | How you use it |
|-------|----------------|
| Host tools | Capability groups **nms / common / topology** (one group ↔ one skill). Default nms+common in Ops preset; topology and all public off. Other agents may mount `dsh-netxops/tools-*` |
| Host tools | Capability groups **ops / topology** (one group ↔ one skill). Default ops in Ops preset; topology and all public off. Other agents may mount `dsh-netxops/tools-ops|topology` |
| Plugins card | Settings → Plugins → **Netx Ops** (URL / lang / token / capability groups) |
| Agent preset + skills | Settings → Agent presets → **Custom → Netx Ops** (copied into `~/.dsh/.agent-presets` on first boot); playbooks follow the same group toggles |
@ -31,9 +31,9 @@ dsh web # or: pnpm dsh web
1. **Settings → Plugins → Netx Ops** → API URL (+ token if the field is enabled).
Token fallback: `scripts/set-netx-token.ps1` / `.sh`.
Optional: enable **关键告警推送** so this DSH dials `ws(s)://<apiUrl>/v1/integrations/dsh-alarm/ws`. Choose **投递到 DSH 会话** and/or **投递到 WhatsApp / IM**. For IM, install `dsh-im-ops`, create a delivery target, then paste `imBotId` / `imTargetId`. The card header shows live WSS status.
Capability groups: leave defaults for nms+common, or enable **topology** / **对其他预设公开** (new sessions after save).
Capability groups: leave default for **ops**, or enable **topology** / **对其他预设公开** (new sessions after save).
2. Restart or open Settings → **Agent presets** → Custom → **Netx Ops** should appear after the host plugin has activated once.
3. **New session → preset Netx Ops** → ask e.g. Critical Top / single-host alarms.
3. **New session → preset Netx Ops** → ask e.g. Critical Top / single-host alarms / 「能否登录」.
## Key-alarm push
@ -47,7 +47,7 @@ dsh web # or: pnpm dsh web
1. Plugins card **Netx Ops** visible.
2. With key-alarm push on: card badge shows **Connected** (or reconnecting / auth failed with detail).
3. Agent presets → Custom → **Netx Ops**.
4. Tools include `netx__queryNmsAlarms` (**nms**). `netx__findTopologyPaths` is **common**. Canvas / dual_unit need **topology** (`netx-topology` skill).
4. Tools include `netx__queryNmsAlarms` / `netx__execManagedNe` / `netx__findTopologyPaths` (**ops** → skill `netx-ops`). Canvas / dual_unit need **topology** (`netx-topology`).
See [TOOL_MAP.md](TOOL_MAP.md).

View file

@ -5,8 +5,7 @@
| Source (oclaw) | Destination |
|----------------|-------------|
| `runtime/workspaces/ops/ROLE_SYSTEM.md` | `presets/netxops/PERSONA.md` + persona in `agent.cordis.yml` (brand **Netx Ops**) |
| `skills/_workspace/ops/ops-netx-ume-playbook/` | **Canonical:** `netx/skills/nms/netx-nms/` |
| `skills/_workspace/ops/ops-netx-managed-ne-playbook/` | **Canonical:** `netx/skills/common/netx-common/` |
| `skills/_workspace/ops/ops-netx-ume-playbook/` + `ops-netx-managed-ne-playbook/` | **Canonical:** `netx/skills/ops/netx-ops/` (merged ops group) |
| `skills/.../netx-topology` (MCP) | **Canonical:** `netx/skills/topology/netx-topology/` |
| netx MCP HTTP surface | Host plugin `src/netx/*` + `src/index.ts`: settings namespace `netxops` + credential `NETX_API_TOKEN` → native `netx__*` tools |

View file

@ -4,26 +4,20 @@ Tools register by capability group. **One group ↔ one skill.**
| Group | Skill | Default in Ops preset |
|-------|-------|------------------------|
| **nms** | `netx-nms` | on |
| **common** | `netx-common` | on |
| **ops** | `netx-ops` | on |
| **topology** | `netx-topology` | off |
Public flags mount the same group on the host for other presets.
Forced mounts: `dsh-netxops/tools-nms` | `tools-common` | `tools-topology`.
Forced mounts: `dsh-netxops/tools-ops` | `tools-topology` (legacy aliases: `tools-nms` / `tools-common` → ops).
Model names: `netx__<stem>`. NMS tools use `Nms`; adapter `nmsProvider=zte-ume` still hits `/v1/ume/*`.
## nms → `netx-nms`
## ops → `netx-ops`
| Tool | Role |
|------|------|
| `queryNmsAlarms` … `sqlQueryNms` | Alarm + inventory + SQL |
## common → `netx-common`
| Tool | Role |
|------|------|
| `listManagedNe` / `getManagedNe` / `execManagedNe` / `listCliTargets` | Managed CLI |
| `listManagedNe` / `getManagedNe` / `execManagedNe` / `listCliTargets` | Managed CLI (login / show) |
| `findTopologyPaths` | Fabric path lookup |
## topology → `netx-topology`

View file

@ -1,11 +1,10 @@
// src/netx/capability-groups.ts
var DEFAULT_CAPABILITY_GROUPS = Object.freeze({
nms: Object.freeze({ inPreset: true, public: false }),
common: Object.freeze({ inPreset: true, public: false }),
ops: Object.freeze({ inPreset: true, public: false }),
topology: Object.freeze({ inPreset: false, public: false })
});
var TOOLS_BY_GROUP = Object.freeze({
nms: Object.freeze([
ops: Object.freeze([
"netx__queryNmsAlarms",
"netx__aggregateNmsAlarms",
"netx__runNmsDiagnostics",
@ -14,9 +13,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__queryNmsAlarmsRaw",
"netx__aggregateNmsAlarmsRaw",
"netx__listNmsAlarmFields",
"netx__sqlQueryNms"
]),
common: Object.freeze([
"netx__sqlQueryNms",
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
@ -43,13 +40,11 @@ var TOOLS_BY_GROUP = Object.freeze({
])
});
var SKILL_DIR_BY_GROUP = Object.freeze({
nms: "nms",
common: "common",
ops: "ops",
topology: "topology"
});
var CAPABILITY_GROUP_IDS = Object.freeze([
"nms",
"common",
"ops",
"topology"
]);
function groupsForPlane(groups, plane, only) {
@ -61,7 +56,8 @@ function groupsForPlane(groups, plane, only) {
return enabled.filter((id) => allow.has(id));
}
function groupsForced(only) {
return CAPABILITY_GROUP_IDS.filter((id) => only.includes(id));
const normalized = only.map((id) => id === "nms" || id === "common" ? "ops" : id);
return CAPABILITY_GROUP_IDS.filter((id) => normalized.includes(id));
}
function toolNamesForGroups(groupIds) {
const names = new Set;
@ -1792,7 +1788,7 @@ function applyGroupToolsPlugin(ctx, options) {
var name = "netxops-tools-common";
var inject = ["tools"];
function apply(ctx) {
applyGroupToolsPlugin(ctx, { name, mode: "forced", only: ["common"] });
applyGroupToolsPlugin(ctx, { name, mode: "forced", only: ["ops"] });
}
export {
name,

View file

@ -1,11 +1,10 @@
// src/netx/capability-groups.ts
var DEFAULT_CAPABILITY_GROUPS = Object.freeze({
nms: Object.freeze({ inPreset: true, public: false }),
common: Object.freeze({ inPreset: true, public: false }),
ops: Object.freeze({ inPreset: true, public: false }),
topology: Object.freeze({ inPreset: false, public: false })
});
var TOOLS_BY_GROUP = Object.freeze({
nms: Object.freeze([
ops: Object.freeze([
"netx__queryNmsAlarms",
"netx__aggregateNmsAlarms",
"netx__runNmsDiagnostics",
@ -14,9 +13,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__queryNmsAlarmsRaw",
"netx__aggregateNmsAlarmsRaw",
"netx__listNmsAlarmFields",
"netx__sqlQueryNms"
]),
common: Object.freeze([
"netx__sqlQueryNms",
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
@ -43,13 +40,11 @@ var TOOLS_BY_GROUP = Object.freeze({
])
});
var SKILL_DIR_BY_GROUP = Object.freeze({
nms: "nms",
common: "common",
ops: "ops",
topology: "topology"
});
var CAPABILITY_GROUP_IDS = Object.freeze([
"nms",
"common",
"ops",
"topology"
]);
function groupsForPlane(groups, plane, only) {
@ -61,7 +56,8 @@ function groupsForPlane(groups, plane, only) {
return enabled.filter((id) => allow.has(id));
}
function groupsForced(only) {
return CAPABILITY_GROUP_IDS.filter((id) => only.includes(id));
const normalized = only.map((id) => id === "nms" || id === "common" ? "ops" : id);
return CAPABILITY_GROUP_IDS.filter((id) => normalized.includes(id));
}
function toolNamesForGroups(groupIds) {
const names = new Set;
@ -1792,7 +1788,7 @@ function applyGroupToolsPlugin(ctx, options) {
var name = "netxops-tools-nms";
var inject = ["tools"];
function apply(ctx) {
applyGroupToolsPlugin(ctx, { name, mode: "forced", only: ["nms"] });
applyGroupToolsPlugin(ctx, { name, mode: "forced", only: ["ops"] });
}
export {
name,

1797
lib/agent-tools-ops.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,11 +1,10 @@
// src/netx/capability-groups.ts
var DEFAULT_CAPABILITY_GROUPS = Object.freeze({
nms: Object.freeze({ inPreset: true, public: false }),
common: Object.freeze({ inPreset: true, public: false }),
ops: Object.freeze({ inPreset: true, public: false }),
topology: Object.freeze({ inPreset: false, public: false })
});
var TOOLS_BY_GROUP = Object.freeze({
nms: Object.freeze([
ops: Object.freeze([
"netx__queryNmsAlarms",
"netx__aggregateNmsAlarms",
"netx__runNmsDiagnostics",
@ -14,9 +13,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__queryNmsAlarmsRaw",
"netx__aggregateNmsAlarmsRaw",
"netx__listNmsAlarmFields",
"netx__sqlQueryNms"
]),
common: Object.freeze([
"netx__sqlQueryNms",
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
@ -43,13 +40,11 @@ var TOOLS_BY_GROUP = Object.freeze({
])
});
var SKILL_DIR_BY_GROUP = Object.freeze({
nms: "nms",
common: "common",
ops: "ops",
topology: "topology"
});
var CAPABILITY_GROUP_IDS = Object.freeze([
"nms",
"common",
"ops",
"topology"
]);
function groupsForPlane(groups, plane, only) {
@ -61,7 +56,8 @@ function groupsForPlane(groups, plane, only) {
return enabled.filter((id) => allow.has(id));
}
function groupsForced(only) {
return CAPABILITY_GROUP_IDS.filter((id) => only.includes(id));
const normalized = only.map((id) => id === "nms" || id === "common" ? "ops" : id);
return CAPABILITY_GROUP_IDS.filter((id) => normalized.includes(id));
}
function toolNamesForGroups(groupIds) {
const names = new Set;

View file

@ -1,11 +1,10 @@
// src/netx/capability-groups.ts
var DEFAULT_CAPABILITY_GROUPS = Object.freeze({
nms: Object.freeze({ inPreset: true, public: false }),
common: Object.freeze({ inPreset: true, public: false }),
ops: Object.freeze({ inPreset: true, public: false }),
topology: Object.freeze({ inPreset: false, public: false })
});
var TOOLS_BY_GROUP = Object.freeze({
nms: Object.freeze([
ops: Object.freeze([
"netx__queryNmsAlarms",
"netx__aggregateNmsAlarms",
"netx__runNmsDiagnostics",
@ -14,9 +13,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__queryNmsAlarmsRaw",
"netx__aggregateNmsAlarmsRaw",
"netx__listNmsAlarmFields",
"netx__sqlQueryNms"
]),
common: Object.freeze([
"netx__sqlQueryNms",
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
@ -43,13 +40,11 @@ var TOOLS_BY_GROUP = Object.freeze({
])
});
var SKILL_DIR_BY_GROUP = Object.freeze({
nms: "nms",
common: "common",
ops: "ops",
topology: "topology"
});
var CAPABILITY_GROUP_IDS = Object.freeze([
"nms",
"common",
"ops",
"topology"
]);
function groupsForPlane(groups, plane, only) {
@ -61,7 +56,8 @@ function groupsForPlane(groups, plane, only) {
return enabled.filter((id) => allow.has(id));
}
function groupsForced(only) {
return CAPABILITY_GROUP_IDS.filter((id) => only.includes(id));
const normalized = only.map((id) => id === "nms" || id === "common" ? "ops" : id);
return CAPABILITY_GROUP_IDS.filter((id) => normalized.includes(id));
}
function toolNamesForGroups(groupIds) {
const names = new Set;

View file

@ -630,31 +630,17 @@ function NetxopsCard(props) {
children: t("capabilityGroupsHint")
}),
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
title: t("groupNms"),
title: t("groupOps"),
inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"),
inPreset: state.groupNmsInPreset,
published: state.groupNmsPublic,
inPreset: state.groupOpsInPreset,
published: state.groupOpsPublic,
disabled,
onEditInPreset: (checked) => {
props.edit("groupNmsInPreset", checked ? "true" : "false");
props.edit("groupOpsInPreset", checked ? "true" : "false");
},
onEditPublic: (checked) => {
props.edit("groupNmsPublic", checked ? "true" : "false");
}
}),
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
title: t("groupCommon"),
inPresetLabel: t("groupInPreset"),
publicLabel: t("groupPublic"),
inPreset: state.groupCommonInPreset,
published: state.groupCommonPublic,
disabled,
onEditInPreset: (checked) => {
props.edit("groupCommonInPreset", checked ? "true" : "false");
},
onEditPublic: (checked) => {
props.edit("groupCommonPublic", checked ? "true" : "false");
props.edit("groupOpsPublic", checked ? "true" : "false");
}
}),
/* @__PURE__ */ jsx_runtime.jsx(CapabilityGroupBlock, {
@ -1162,10 +1148,8 @@ class NetxopsCardController {
textField("apiUrl"),
textField("lang"),
textField("nmsProvider"),
booleanFieldPersistFalse("groupNmsInPreset"),
booleanField("groupNmsPublic"),
booleanFieldPersistFalse("groupCommonInPreset"),
booleanField("groupCommonPublic"),
booleanFieldPersistFalse("groupOpsInPreset"),
booleanField("groupOpsPublic"),
booleanField("groupTopologyInPreset"),
booleanField("groupTopologyPublic"),
booleanField("alarmPushEnabled"),
@ -1269,10 +1253,8 @@ class NetxopsCardController {
apiUrl: this.form.field("apiUrl"),
lang: this.form.field("lang"),
nmsProvider: this.form.field("nmsProvider"),
groupNmsInPreset: this.form.field("groupNmsInPreset"),
groupNmsPublic: this.form.field("groupNmsPublic"),
groupCommonInPreset: this.form.field("groupCommonInPreset"),
groupCommonPublic: this.form.field("groupCommonPublic"),
groupOpsInPreset: this.form.field("groupOpsInPreset"),
groupOpsPublic: this.form.field("groupOpsPublic"),
groupTopologyInPreset: this.form.field("groupTopologyInPreset"),
groupTopologyPublic: this.form.field("groupTopologyPublic"),
alarmPushEnabled: this.form.field("alarmPushEnabled"),
@ -1350,17 +1332,16 @@ function refOf(snapshot) {
// src/client/locales.ts
var en = {
title: "Netx Ops",
description: "netx API, bearer token, and capability groups (nms / common / topology).",
description: "netx API, bearer token, and capability groups (ops / topology).",
apiUrl: "API URL",
apiUrlHint: "netx REST root, e.g. http://127.0.0.1:8890 (also used for alarm subscribe)",
lang: "Language",
langHint: "Response language hint (zh / en).",
capabilityGroups: "Capability groups",
capabilityGroupsHint: "One group ↔ one skill (netx-nms / netx-common / netx-topology). Defaults: nms + common in preset; topology and all public off. Save, then open a new session. Other agents can mount dsh-netxops/tools-nms|common|topology.",
capabilityGroupsHint: "One group ↔ one skill. ops = netx-ops (alarms + CLI login + paths); topology = netx-topology. Defaults: ops on; topology and public off. New sessions after save. Other agents: dsh-netxops/tools-ops|topology.",
nmsProvider: "nms provider",
nmsProviderHint: "Vendor adapter id. Supported today: zte-ume (REST still /v1/ume/*; model tools are netx__*Nms*).",
groupNms: "nms — skill netx-nms (alarms / inventory / SQL)",
groupCommon: "common — skill netx-common (managed CLI + findTopologyPaths)",
groupOps: "ops — skill netx-ops (NMS alarms/inventory + managed CLI login + paths)",
groupTopology: "topology — skill netx-topology (canvas / fabric / dual_unit)",
groupInPreset: "In Netx Ops preset",
groupPublic: "Publish to other presets",
@ -1406,17 +1387,16 @@ var en = {
};
var zh = {
title: "Netx Ops",
description: "netx API、Bearer Token,以及能力组(nms / common / topology)。",
description: "netx API、Bearer Token,以及能力组(ops / topology)。",
apiUrl: "API 地址",
apiUrlHint: "netx REST 根地址,例如 http://127.0.0.1:8890(告警订阅复用同一地址)",
lang: "语言",
langHint: "响应语言提示(zh / en)。",
capabilityGroups: "能力组",
capabilityGroupsHint: "一组一个 skill(netx-nms / netx-common / netx-topology)。默认:nms + common 进入预设;topology 与全部公开关闭。保存后请新开会话。其他 Agent 可挂 dsh-netxops/tools-nms|common|topology。",
capabilityGroupsHint: "一组一个 skill。ops = netx-ops(告警 + 纳管登录 + 路径);topology = netx-topology。默认 ops 开;topology / 公开关。保存后新会话生效。其他 Agent:dsh-netxops/tools-ops|topology。",
nmsProvider: "nms provider",
nmsProviderHint: "厂商适配器 id。当前支持:zte-ume(REST 仍为 /v1/ume/*;模型工具名为 netx__*Nms*)。",
groupNms: "nms — skill netx-nms(告警 / 库存 / SQL)",
groupCommon: "common — skill netx-common(纳管 CLI + findTopologyPaths)",
groupOps: "ops — skill netx-ops(NMS 告警/库存 + 纳管 CLI 登录 + 路径)",
groupTopology: "topology — skill netx-topology(画布 / Fabric / dual_unit)",
groupInPreset: "在 Netx Ops 预设中启用",
groupPublic: "对其他预设公开",

View file

@ -395,12 +395,11 @@ function resetAlarmSession() {
// src/netx/capability-groups.ts
var DEFAULT_CAPABILITY_GROUPS = Object.freeze({
nms: Object.freeze({ inPreset: true, public: false }),
common: Object.freeze({ inPreset: true, public: false }),
ops: Object.freeze({ inPreset: true, public: false }),
topology: Object.freeze({ inPreset: false, public: false })
});
var TOOLS_BY_GROUP = Object.freeze({
nms: Object.freeze([
ops: Object.freeze([
"netx__queryNmsAlarms",
"netx__aggregateNmsAlarms",
"netx__runNmsDiagnostics",
@ -409,9 +408,7 @@ var TOOLS_BY_GROUP = Object.freeze({
"netx__queryNmsAlarmsRaw",
"netx__aggregateNmsAlarmsRaw",
"netx__listNmsAlarmFields",
"netx__sqlQueryNms"
]),
common: Object.freeze([
"netx__sqlQueryNms",
"netx__listManagedNe",
"netx__getManagedNe",
"netx__execManagedNe",
@ -438,27 +435,32 @@ var TOOLS_BY_GROUP = Object.freeze({
])
});
var SKILL_DIR_BY_GROUP = Object.freeze({
nms: "nms",
common: "common",
ops: "ops",
topology: "topology"
});
var CAPABILITY_GROUP_IDS = Object.freeze([
"nms",
"common",
"ops",
"topology"
]);
function capabilityGroupsFromSettings(fields) {
const src = fields ?? {};
const commonInPreset = src.groupCommonInPreset !== undefined ? src.groupCommonInPreset !== false : src.groupManagedNeInPreset !== false;
const commonPublic = src.groupCommonPublic === true || src.groupManagedNePublic === true;
let opsInPreset = true;
if (src.groupOpsInPreset !== undefined) {
opsInPreset = src.groupOpsInPreset !== false;
} else {
const legacy = [
src.groupNmsInPreset,
src.groupCommonInPreset,
src.groupManagedNeInPreset
].filter((v) => v !== undefined);
if (legacy.length > 0) {
opsInPreset = legacy.some((v) => v === true);
}
}
return {
nms: {
inPreset: src.groupNmsInPreset !== false,
public: src.groupNmsPublic === true
},
common: {
inPreset: commonInPreset,
public: commonPublic
ops: {
inPreset: opsInPreset,
public: src.groupOpsPublic === true || src.groupNmsPublic === true || src.groupCommonPublic === true || src.groupManagedNePublic === true
},
topology: {
inPreset: src.groupTopologyInPreset === true || src.groupTopologyLayoutInPreset === true,
@ -2152,10 +2154,8 @@ var Config = z.object({
imBotId: z.string().default(""),
imTargetId: z.string().default(""),
nmsProvider: z.string().default("zte-ume"),
groupNmsInPreset: z.boolean().default(true),
groupNmsPublic: z.boolean().default(false),
groupCommonInPreset: z.boolean().default(true),
groupCommonPublic: z.boolean().default(false),
groupOpsInPreset: z.boolean().default(true),
groupOpsPublic: z.boolean().default(false),
groupTopologyInPreset: z.boolean().default(false),
groupTopologyPublic: z.boolean().default(false)
});

View file

@ -1,7 +1,7 @@
{
"name": "dsh-netxops",
"version": "0.1.21",
"description": "DeepSeek Harness Netx Ops: capability groups nms/common/topology (one group ↔ one skill)",
"version": "0.1.22",
"description": "DeepSeek Harness Netx Ops: capability groups ops/topology (one group ↔ one skill)",
"license": "MIT",
"type": "module",
"private": false,
@ -9,6 +9,7 @@
"exports": {
".": "./lib/index.js",
"./tools": "./lib/agent-tools.js",
"./tools-ops": "./lib/agent-tools-ops.js",
"./tools-nms": "./lib/agent-tools-nms.js",
"./tools-common": "./lib/agent-tools-common.js",
"./tools-topology": "./lib/agent-tools-topology.js",
@ -19,6 +20,7 @@
"src/",
"lib/index.js",
"lib/agent-tools.js",
"lib/agent-tools-ops.js",
"lib/agent-tools-nms.js",
"lib/agent-tools-common.js",
"lib/agent-tools-topology.js",

View file

@ -26,8 +26,7 @@ You are **Netx Ops**, a network operations specialist for NMS / netx.
- Display NEs by **host_name** only — never bare UUID to the user.
## Skills (by capability group)
- `netx-nms` — **nms**
- `netx-common` — **common**
- `netx-ops` — **ops** (NMS alarms/inventory + managed CLI login + paths)
- `netx-topology` — **topology** (canvas / dual_unit / layout)
## Tools

View file

@ -34,8 +34,7 @@
- No evidence → say evidence insufficient; do not invent root cause.
## Skills (load before acting; only when the capability group is enabled)
- nms alarms / inventory → `netx-nms`
- common managed CLI + paths → `netx-common`
- ops (alarms / inventory / 「能否登录」CLI / paths) → `netx-ops`
- topology canvas / dual_unit / layout → `netx-topology`
## Tools

View file

@ -7,4 +7,4 @@ powershell -File .\scripts\sync-skills-from-netx.ps1
`
Runtime prefers `NETX_SKILLS_ROOT` or sibling `../netx/skills`.
One topology skill covers canvas + dual_unit / layout recipes; DSH may still gate **tools** via topology vs topology-layout groups.
Groups: **ops** (`netx-ops`) + **topology** (`netx-topology`). One group 鈫?one skill.

View file

@ -1,91 +0,0 @@
---
name: netx-common
description: >-
netx common playbook (MCP + DSH): managed-NE CLI (SSH/Telnet batch-first) plus
native findTopologyPaths. Not NMS REST alarms/inventory. Trigger: execManagedNe,
show/display, optical, capacity A<>B, path between sites.
---
# netx-common (managed CLI + paths)
## Naming (MCP + DSH)
Canonical: **`netx/skills/common/netx-common/`**. Mirrored into MCP / dsh-netxops — edit once.
| Host | How tools appear |
|------|------------------|
| **MCP** (`netx-mcp`) | Bare names: `execManagedNe`, `findTopologyPaths`, … |
| **DSH** (`dsh-netxops`) | Prefixed: `netx__execManagedNe`, … |
NMS alarms/inventory → **netx-nms**. Canvas / dual_unit → **netx-topology**.
## Tools
| Purpose | Tool |
|---------|------|
| List managed NEs | `listManagedNe` |
| Managed detail | `getManagedNe` |
| Read-only CLI (batch-first) | `execManagedNe` |
| CLI target index | `listCliTargets` |
| Fabric paths | `findTopologyPaths` |
Prefer `nms_ne_id` / `nms_ne_ids`; legacy `ume_*` accepted. `listCliTargets(source=nms)` preferred (`ume` alias).
## CLI order
1. `listManagedNe` (`connect_status=pass`) or `listCliTargets` (**once** per session, cache ids)
2. Multi-NE → **one** `execManagedNe` with `ne_ids` / `nms_ne_ids` / `targets`
3. Paths → `findTopologyPaths` (`nms_ne_id` **or** `managed_ne_id` per end)
### Batch examples
**Same commands, many NEs**
```json
{
"nms_ne_ids": ["uuid-a", "uuid-b", "uuid-c"],
"commands": ["show version"],
"read_timeout_sec": 60,
"concurrency": 4
}
```
**Different commands per NE (still one call)**
```json
{
"targets": [
{"nms_ne_id": "uuid-zte", "commands": ["show opticalinfo brief"]},
{"nms_ne_id": "uuid-hw", "commands": ["display optical-module brief"]},
{"nms_ne_id": "uuid-cisco", "commands": ["show interface transceiver"]}
],
"read_timeout_sec": 90
}
```
**Wrong:** N× single-NE `execManagedNe` in one turn (stdio serial).
## Field recipes
### Capacity / optical A<>B
1. Resolve nicknames → `host_name`
2. `findTopologyPaths` and/or LLDP → both ports
3. Optics CLI on **both** ends; summarize RX/TX / thresholds
4. Do not answer with only NMS bandwidth/optical-power-threshold alarm tallies unless asked
### ZTE optical CLI
Try in order; one failure → switch spelling (do not blind-retry):
| Prefer | Fallback |
|--------|----------|
| `show opticalinfo brief` | Field-confirmed on many ZXR10 |
| `show optical brief` | Some EN platforms |
| `show opticalinfo brief \| begin <if>` | After port known |
## Guardrails
- Never pass NMS alarm UUIDs as managed `ne_id` for `getManagedNe`
- Allowlist prefixes: `show ` / `display ` / `ping ` / `traceroute` …
- Prefer `host_name` for users; keep ids for tool params

View file

@ -1,70 +0,0 @@
---
name: netx-nms
description: >-
netx NMS playbook (MCP + DSH): vendor NMS adapter (zte-ume) for alarm
query/aggregate/diagnostics, NE inventory, raw fields, and read-only SQL.
Trigger: NMS alarms, host_name, Critical Top, LOS, BN EMS, netx ops.
---
# netx-nms (vendor NMS adapter)
## Naming (MCP + DSH)
Canonical playbooks live in **`netx/skills/`** (this file is mirrored into MCP packages / dsh-netxops).
Edit the netx repo copy; run sync scripts — do not maintain divergent forks.
| Host | How tools appear |
|------|------------------|
| **MCP** (`netx-mcp`) | Bare names: `queryNmsAlarms`, … |
| **DSH** (`dsh-netxops`) | Prefixed: `netx__queryNmsAlarms`, … (same camelCase stem) |
REST still `/v1/ume/*` for provider `zte-ume`.
Prefer `nms_ne_id` / `nms_ne_ids`; legacy `ume_*` aliases still work.
Path lookup → **common** skill `netx-common` (`findTopologyPaths`).
Canvas → **netx-topology**(含 dual_unit / 布图;DSH 可把部分布局工具放在实验 tool group)。
## Tool names
| Purpose | Tool |
|---------|------|
| Alarm list | `queryNmsAlarms` |
| Alarm aggregate | `aggregateNmsAlarms` |
| Diagnostics | `runNmsDiagnostics` |
| NE inventory | `queryNmsNeInventory` |
| NE detail | `getNmsNe` |
| Field list | `listNmsAlarmFields` |
| Raw rows | `queryNmsAlarmsRaw` |
| Dynamic aggregate | `aggregateNmsAlarmsRaw` |
| SQL | `sqlQueryNms` |
| Managed CLI (other skill) | `listManagedNe` / `getManagedNe` / `execManagedNe` / `listCliTargets` |
## Tool order
1. **Freshness first**: `runNmsDiagnostics` or `aggregateNmsAlarms` → `meta.last_seen_min` / `last_seen_max`.
2. Overview: `aggregateNmsAlarms` + `runNmsDiagnostics`; samples via `queryNmsAlarms` (one page).
3. Evidence: `listNmsAlarmFields` → `queryNmsAlarmsRaw` (`field_preset=evidence`).
4. Custom aggregate: `aggregateNmsAlarmsRaw`.
5. SQL: `sqlQueryNms` (SELECT only; `statement_timeout_ms`).
6. Paths: `findTopologyPaths` via **netx-common**.
7. Device CLI: **netx-common** — multi-NE = **one** `execManagedNe` batch.
## Short-intent recipes
| User says | Recipe |
|-----------|--------|
| fiber cut / LOS / 断纤 | `queryNmsAlarmsRaw(keyword=LOS)` and/or `Fiber Break` → **host_name** list |
| offline / BN EMS | keyword=`BN EMS` |
| Critical Top | `aggregateNmsAlarms(severity=critical, top_ne=20)` |
| CRC in area | Raw `keyword=CRC` + `AREA-` hostname prefix |
| optical power **threshold** | keyword=`optical power` + area — **not** fiber-cut |
| one hostname | host-scoped `queryNmsAlarms` / Raw only |
## Guardrails
- Prefer non-SQL; filter severity → keyword/host → time → ne_id.
- Lists ≤2 pages; `page_size` default 50.
- Display **host_name** only; never bare UUID to users.
- `getManagedNe` needs managed id; NMS UUID → `getNmsNe` / `execManagedNe(nms_ne_id=...)`.
See [reference.md](reference.md).

View file

@ -1,20 +0,0 @@
# netx-nms quick reference
## Freshness
- `runNmsDiagnostics` / `aggregateNmsAlarms` → `meta.last_seen_min` / `last_seen_max`
- Snapshot: windows inside min~max — do not default to `now()-30m`
## Tools
| Intent | Call |
|--------|------|
| Critical Top | `aggregateNmsAlarms(severity=critical, top_ne=20)` |
| Fiber / LOS | Raw `keyword=LOS` / `Fiber Break` |
| Offline / BN EMS | Raw keyword=`BN EMS` |
| Single host | `queryNmsAlarms(host_name=…)` |
| Inventory | `queryNmsNeInventory(keyword=…)` / `getNmsNe` |
| Paths | `findTopologyPaths(from_nms_ne_id, to_nms_ne_id)` — **common** skill |
| SQL | `sqlQueryNms` SELECT; `statement_timeout_ms=8000` |
DSH: prefix every tool with `netx__`.

View file

@ -0,0 +1,94 @@
---
name: netx-ops
description: >-
Netx Ops playbook (MCP + DSH): NMS alarms/inventory/SQL plus managed-NE CLI
login (execManagedNe batch-first) and findTopologyPaths. Trigger: alarms,
host_name, Critical Top, LOS, 能否登录, show/display, optical, capacity A<>B,
path between sites, netx ops.
---
# netx-ops(告警 + 纳管登录)
**一组一个 skill。** 问「能否登录 / CLI / show」必须走本 skill 的 managed 工具,不要只查 NMS inventory。
## Hosts
| Host | Tools |
|------|--------|
| **MCP** `netx-mcp` | 裸名 `queryNmsAlarms` / `execManagedNe` / … |
| **DSH** `dsh-netxops` | `netx__` + 同 stem;能力组 **ops**(默认开) |
REST NMS 仍 `/v1/ume/*`(`nmsProvider=zte-ume`)。优先 `nms_ne_id`;`ume_*` 别名可用。
画布 / dual_unit → **netx-topology**(能力组 topology)。
## Tools
### NMS
| Purpose | Tool |
|---------|------|
| Alarm list / aggregate / diagnostics | `queryNmsAlarms` / `aggregateNmsAlarms` / `runNmsDiagnostics` |
| Inventory / detail | `queryNmsNeInventory` / `getNmsNe` |
| Raw / fields / SQL | `queryNmsAlarmsRaw` / `listNmsAlarmFields` / `aggregateNmsAlarmsRaw` / `sqlQueryNms` |
### Managed CLI + paths
| Purpose | Tool |
|---------|------|
| List CLI targets | `listManagedNe` / `listCliTargets` |
| Managed detail | `getManagedNe`(**仅**纳管 id) |
| Login / show | `execManagedNe`(batch-first) |
| Fabric paths | `findTopologyPaths` |
## 「能否登录」决策树(强制)
1. `listCliTargets(keyword=host_or_ip)` 或 `listManagedNe(keyword=…, connect_status=pass)`
- 命中 → `execManagedNe(ne_id|nms_ne_id, commands=["show version"])` **验证真正能登**
- 未命中 → `queryNmsNeInventory(keyword=…)` 说明 NMS 有无 / `connection_status`,并明确:**未纳管 netx CLI 则不能用本通道登录**
2. 禁止只查 inventory 就下「不能登录」或「能登录」结论而不尝试 `execManagedNe`(已纳管时)。
3. NMS UUID 不要塞进 `getManagedNe`;用 `execManagedNe(nms_ne_id=…)` 或先 list 拿 managed `ne_id`。
## NMS 工具顺序
1. Freshness: `runNmsDiagnostics` / `aggregateNmsAlarms` → `meta.last_seen_*`
2. Overview + one-page `queryNmsAlarms`
3. Evidence: `queryNmsAlarmsRaw` (`field_preset=evidence`)
4. Paths / CLI as needed (same skill)
## CLI order
1. `listManagedNe` / `listCliTargets`(每会话最多一次,缓存 id)
2. 多台 → **一次** `execManagedNe`(`ne_ids` / `nms_ne_ids` / `targets`)
3. 路径 → `findTopologyPaths`
### Batch 示例
```json
{ "nms_ne_ids": ["uuid-a", "uuid-b"], "commands": ["show version"], "concurrency": 4 }
```
```json
{
"targets": [
{"nms_ne_id": "uuid-zte", "commands": ["show opticalinfo brief"]},
{"nms_ne_id": "uuid-hw", "commands": ["display optical-module brief"]}
]
}
```
## Short recipes
| User says | Recipe |
|-----------|--------|
| 能否登录 / login / SSH | 上表「能否登录」决策树 |
| fiber / LOS | Raw `keyword=LOS` / `Fiber Break` |
| Critical Top | `aggregateNmsAlarms(severity=critical, top_ne=20)` |
| capacity A<>B | paths / LLDP → 两端 optic CLI(一批) |
## Guardrails
- 展示 **host_name**;勿对用户甩裸 UUID
- CLI 白名单:`show` / `display` / `ping` / `traceroute` …
- 同轮禁止 N× 单台 `execManagedNe`
See [reference.md](reference.md).

View file

@ -0,0 +1,22 @@
# netx-ops quick reference
## Can it log in?
1. `listCliTargets` / `listManagedNe` → if found, `execManagedNe(…, commands=["show version"])`
2. Else `queryNmsNeInventory` → report NMS presence; say CLI not managed if absent from managed list
## NMS freshness
- `runNmsDiagnostics` / `aggregateNmsAlarms` → `meta.last_seen_min` / `max`
## Shortcuts
| Intent | Call |
|--------|------|
| Critical Top | `aggregateNmsAlarms(severity=critical, top_ne=20)` |
| Fiber / LOS | Raw `keyword=LOS` / `Fiber Break` |
| Inventory | `queryNmsNeInventory(keyword=…)` / `getNmsNe` |
| CLI batch | `execManagedNe(nms_ne_ids=[…], commands=[…])` |
| Paths | `findTopologyPaths(from_nms_ne_id, to_nms_ne_id)` |
DSH: prefix tools with `netx__`.

View file

@ -30,6 +30,7 @@ const external = [
const entries = [
{ entry: join(root, 'src/index.ts'), out: join(outDir, 'index.js') },
{ entry: join(root, 'src/agent-tools.ts'), out: join(outDir, 'agent-tools.js') },
{ entry: join(root, 'src/agent-tools-ops.ts'), out: join(outDir, 'agent-tools-ops.js') },
{ entry: join(root, 'src/agent-tools-nms.ts'), out: join(outDir, 'agent-tools-nms.js') },
{ entry: join(root, 'src/agent-tools-common.ts'), out: join(outDir, 'agent-tools-common.js') },
{ entry: join(root, 'src/agent-tools-topology.ts'), out: join(outDir, 'agent-tools-topology.js') },

View file

@ -17,7 +17,7 @@ $src = Join-Path $NetxRoot "skills"
$dst = Join-Path $opsRoot "presets\netxops\skills"
if (-not (Test-Path $src)) { throw "missing $src" }
foreach ($group in @("nms", "common", "topology")) {
foreach ($group in @("ops", "topology")) {
$from = Join-Path $src $group
$to = Join-Path $dst $group
if (-not (Test-Path $from)) { throw "missing group $from" }
@ -26,9 +26,11 @@ foreach ($group in @("nms", "common", "topology")) {
Copy-Item $from $to -Recurse -Force
}
# Drop legacy separate layout skill if present
$legacyLayout = Join-Path $dst "topology-layout"
if (Test-Path $legacyLayout) { Remove-Item $legacyLayout -Recurse -Force }
# Drop legacy groups if present
foreach ($legacy in @("nms", "common", "topology-layout")) {
$legacyPath = Join-Path $dst $legacy
if (Test-Path $legacyPath) { Remove-Item $legacyPath -Recurse -Force }
}
@"
# Mirrored from netx/skills — do not long-edit here
@ -40,7 +42,7 @@ powershell -File .\scripts\sync-skills-from-netx.ps1
```
Runtime prefers ``NETX_SKILLS_ROOT`` or sibling ``../netx/skills``.
One topology skill covers canvas + dual_unit / layout recipes; DSH may still gate **tools** via topology vs topology-layout groups.
Groups: **ops** (``netx-ops``) + **topology** (``netx-topology``). One group ↔ one skill.
"@ | Set-Content -Encoding utf8 (Join-Path $dst "README.md")
Write-Host "Synced $src -> $dst"

View file

@ -1,6 +1,5 @@
/**
* Forced common-group tools + skill (native managed CLI + findTopologyPaths).
*
* @deprecated Use `dsh-netxops/tools-ops`. Alias that still forces the ops group.
* @module dsh-netxops/tools-common
*/
@ -11,5 +10,5 @@ export const name = 'netxops-tools-common'
export const inject = ['tools']
export function apply(ctx: Context): void {
applyGroupToolsPlugin(ctx, { name, mode: 'forced', only: ['common'] })
applyGroupToolsPlugin(ctx, { name, mode: 'forced', only: ['ops'] })
}

View file

@ -1,7 +1,5 @@
/**
* Forced nms-group tools + skill (vendor NMS adapter; current provider zte-ume).
* Mount this export in any agent preset that needs NMS alarms/inventory.
*
* @deprecated Use `dsh-netxops/tools-ops`. Alias that still forces the ops group.
* @module dsh-netxops/tools-nms
*/
@ -12,5 +10,5 @@ export const name = 'netxops-tools-nms'
export const inject = ['tools']
export function apply(ctx: Context): void {
applyGroupToolsPlugin(ctx, { name, mode: 'forced', only: ['nms'] })
applyGroupToolsPlugin(ctx, { name, mode: 'forced', only: ['ops'] })
}

15
src/agent-tools-ops.ts Normal file
View file

@ -0,0 +1,15 @@
/**
* Forced ops-group tools + skill (NMS + managed CLI + paths).
*
* @module dsh-netxops/tools-ops
*/
import type { Context } from '@deepseek-ai/cordis'
import { applyGroupToolsPlugin } from './netx/group-tools-plugin.ts'
export const name = 'netxops-tools-ops'
export const inject = ['tools']
export function apply(ctx: Context): void {
applyGroupToolsPlugin(ctx, { name, mode: 'forced', only: ['ops'] })
}

View file

@ -364,31 +364,17 @@ export function NetxopsCard(props: NetxopsCardProps) {
</div>
<p className="dsh-nx-hint">{t('capabilityGroupsHint')}</p>
<CapabilityGroupBlock
title={t('groupNms')}
title={t('groupOps')}
inPresetLabel={t('groupInPreset')}
publicLabel={t('groupPublic')}
inPreset={state.groupNmsInPreset}
published={state.groupNmsPublic}
inPreset={state.groupOpsInPreset}
published={state.groupOpsPublic}
disabled={disabled}
onEditInPreset={(checked) => {
props.edit('groupNmsInPreset', checked ? 'true' : 'false')
props.edit('groupOpsInPreset', checked ? 'true' : 'false')
}}
onEditPublic={(checked) => {
props.edit('groupNmsPublic', checked ? 'true' : 'false')
}}
/>
<CapabilityGroupBlock
title={t('groupCommon')}
inPresetLabel={t('groupInPreset')}
publicLabel={t('groupPublic')}
inPreset={state.groupCommonInPreset}
published={state.groupCommonPublic}
disabled={disabled}
onEditInPreset={(checked) => {
props.edit('groupCommonInPreset', checked ? 'true' : 'false')
}}
onEditPublic={(checked) => {
props.edit('groupCommonPublic', checked ? 'true' : 'false')
props.edit('groupOpsPublic', checked ? 'true' : 'false')
}}
/>
<CapabilityGroupBlock

View file

@ -35,10 +35,8 @@ export interface NetxopsSettings {
alarmDeliverIm?: boolean
imBotId?: string
imTargetId?: string
groupNmsInPreset?: boolean
groupNmsPublic?: boolean
groupCommonInPreset?: boolean
groupCommonPublic?: boolean
groupOpsInPreset?: boolean
groupOpsPublic?: boolean
groupTopologyInPreset?: boolean
groupTopologyPublic?: boolean
nmsProvider?: string
@ -54,10 +52,8 @@ interface CredentialState {
export interface NetxopsCardState extends CardShell {
apiUrl: CardFieldState
lang: CardFieldState
groupNmsInPreset: CardFieldState
groupNmsPublic: CardFieldState
groupCommonInPreset: CardFieldState
groupCommonPublic: CardFieldState
groupOpsInPreset: CardFieldState
groupOpsPublic: CardFieldState
groupTopologyInPreset: CardFieldState
groupTopologyPublic: CardFieldState
nmsProvider: CardFieldState
@ -113,10 +109,8 @@ export class NetxopsCardController {
textField('apiUrl'),
textField('lang'),
textField('nmsProvider'),
booleanFieldPersistFalse('groupNmsInPreset'),
booleanField('groupNmsPublic'),
booleanFieldPersistFalse('groupCommonInPreset'),
booleanField('groupCommonPublic'),
booleanFieldPersistFalse('groupOpsInPreset'),
booleanField('groupOpsPublic'),
booleanField('groupTopologyInPreset'),
booleanField('groupTopologyPublic'),
booleanField('alarmPushEnabled'),
@ -244,10 +238,8 @@ export class NetxopsCardController {
apiUrl: this.form.field('apiUrl'),
lang: this.form.field('lang'),
nmsProvider: this.form.field('nmsProvider'),
groupNmsInPreset: this.form.field('groupNmsInPreset'),
groupNmsPublic: this.form.field('groupNmsPublic'),
groupCommonInPreset: this.form.field('groupCommonInPreset'),
groupCommonPublic: this.form.field('groupCommonPublic'),
groupOpsInPreset: this.form.field('groupOpsInPreset'),
groupOpsPublic: this.form.field('groupOpsPublic'),
groupTopologyInPreset: this.form.field('groupTopologyInPreset'),
groupTopologyPublic: this.form.field('groupTopologyPublic'),
alarmPushEnabled: this.form.field('alarmPushEnabled'),

View file

@ -11,8 +11,7 @@ export type NetxopsLocaleKey =
| 'capabilityGroupsHint'
| 'nmsProvider'
| 'nmsProviderHint'
| 'groupNms'
| 'groupCommon'
| 'groupOps'
| 'groupTopology'
| 'groupInPreset'
| 'groupPublic'
@ -58,17 +57,16 @@ export type NetxopsLocaleKey =
export const en: Record<NetxopsLocaleKey, string> = {
title: 'Netx Ops',
description: 'netx API, bearer token, and capability groups (nms / common / topology).',
description: 'netx API, bearer token, and capability groups (ops / topology).',
apiUrl: 'API URL',
apiUrlHint: 'netx REST root, e.g. http://127.0.0.1:8890 (also used for alarm subscribe)',
lang: 'Language',
langHint: 'Response language hint (zh / en).',
capabilityGroups: 'Capability groups',
capabilityGroupsHint: 'One group ↔ one skill (netx-nms / netx-common / netx-topology). Defaults: nms + common in preset; topology and all public off. Save, then open a new session. Other agents can mount dsh-netxops/tools-nms|common|topology.',
capabilityGroupsHint: 'One group ↔ one skill. ops = netx-ops (alarms + CLI login + paths); topology = netx-topology. Defaults: ops on; topology and public off. New sessions after save. Other agents: dsh-netxops/tools-ops|topology.',
nmsProvider: 'nms provider',
nmsProviderHint: 'Vendor adapter id. Supported today: zte-ume (REST still /v1/ume/*; model tools are netx__*Nms*).',
groupNms: 'nms — skill netx-nms (alarms / inventory / SQL)',
groupCommon: 'common — skill netx-common (managed CLI + findTopologyPaths)',
groupOps: 'ops — skill netx-ops (NMS alarms/inventory + managed CLI login + paths)',
groupTopology: 'topology — skill netx-topology (canvas / fabric / dual_unit)',
groupInPreset: 'In Netx Ops preset',
groupPublic: 'Publish to other presets',
@ -115,17 +113,16 @@ export const en: Record<NetxopsLocaleKey, string> = {
export const zh: Record<NetxopsLocaleKey, string> = {
title: 'Netx Ops',
description: 'netx API、Bearer Token,以及能力组(nms / common / topology)。',
description: 'netx API、Bearer Token,以及能力组(ops / topology)。',
apiUrl: 'API 地址',
apiUrlHint: 'netx REST 根地址,例如 http://127.0.0.1:8890(告警订阅复用同一地址)',
lang: '语言',
langHint: '响应语言提示(zh / en)。',
capabilityGroups: '能力组',
capabilityGroupsHint: '一组一个 skill(netx-nms / netx-common / netx-topology)。默认:nms + common 进入预设;topology 与全部公开关闭。保存后请新开会话。其他 Agent 可挂 dsh-netxops/tools-nms|common|topology。',
capabilityGroupsHint: '一组一个 skill。ops = netx-ops(告警 + 纳管登录 + 路径);topology = netx-topology。默认 ops 开;topology / 公开关。保存后新会话生效。其他 Agent:dsh-netxops/tools-ops|topology。',
nmsProvider: 'nms provider',
nmsProviderHint: '厂商适配器 id。当前支持:zte-ume(REST 仍为 /v1/ume/*;模型工具名为 netx__*Nms*)。',
groupNms: 'nms — skill netx-nms(告警 / 库存 / SQL)',
groupCommon: 'common — skill netx-common(纳管 CLI + findTopologyPaths)',
groupOps: 'ops — skill netx-ops(NMS 告警/库存 + 纳管 CLI 登录 + 路径)',
groupTopology: 'topology — skill netx-topology(画布 / Fabric / dual_unit)',
groupInPreset: '在 Netx Ops 预设中启用',
groupPublic: '对其他预设公开',

View file

@ -91,14 +91,10 @@ export interface Config {
* (REST `/v1/ume/*`; model tools are generic `netx__*Nms*`).
*/
nmsProvider: string
/** nms tools/skills in the Netx Ops preset (default on). */
groupNmsInPreset: boolean
/** Publish nms tools/skills to other presets via the host layer (default off). */
groupNmsPublic: boolean
/** common tools/skills in the Netx Ops preset (default on). */
groupCommonInPreset: boolean
/** Publish common tools/skills to other presets (default off). */
groupCommonPublic: boolean
/** ops tools/skills in the Netx Ops preset (default on) — NMS + CLI + paths. */
groupOpsInPreset: boolean
/** Publish ops tools/skills to other presets (default off). */
groupOpsPublic: boolean
/** topology canvas / layout tools in the Netx Ops preset (default off). */
groupTopologyInPreset: boolean
/** Publish topology tools/skills to other presets (default off). */
@ -117,10 +113,8 @@ export const Config: z<Config> = z.object({
imBotId: z.string().default(''),
imTargetId: z.string().default(''),
nmsProvider: z.string().default('zte-ume'),
groupNmsInPreset: z.boolean().default(true),
groupNmsPublic: z.boolean().default(false),
groupCommonInPreset: z.boolean().default(true),
groupCommonPublic: z.boolean().default(false),
groupOpsInPreset: z.boolean().default(true),
groupOpsPublic: z.boolean().default(false),
groupTopologyInPreset: z.boolean().default(false),
groupTopologyPublic: z.boolean().default(false),
})

View file

@ -3,13 +3,12 @@
*
* Rule: **one group ↔ one skill** (dirs under `netx/skills/<group>/`).
*
* - `nms` → skill `netx-nms` — vendor NMS adapter (zte-ume behind generic names)
* - `common` → skill `netx-common` — managed CLI + findTopologyPaths
* - `topology` → skill `netx-topology` — canvas / fabric / dual_unit / layout recipes
* - `ops` → skill `netx-ops` — NMS alarms/inventory/SQL + managed CLI + findTopologyPaths
* - `topology` → skill `netx-topology` — canvas / fabric / dual_unit / layout
*/
/** Stable group ids used in settings, skill dirs, and registration filters. */
export type NetxCapabilityGroupId = 'nms' | 'common' | 'topology'
export type NetxCapabilityGroupId = 'ops' | 'topology'
/** Per-group exposure knobs. */
export interface NetxGroupExposure {
@ -24,29 +23,26 @@ export type NetxCapabilityGroups = Record<NetxCapabilityGroupId, NetxGroupExposu
/** Flat settings fields (Plugins card + schemastery Config). */
export interface NetxCapabilityGroupSettingsFields {
groupNmsInPreset: boolean
groupNmsPublic: boolean
groupCommonInPreset: boolean
groupCommonPublic: boolean
groupOpsInPreset: boolean
groupOpsPublic: boolean
groupTopologyInPreset: boolean
groupTopologyPublic: boolean
}
/**
* Default: nms + common in Ops preset; topology and all public off.
* Default: ops in Ops preset; topology and all public off.
*/
export const DEFAULT_CAPABILITY_GROUPS: NetxCapabilityGroups = Object.freeze({
nms: Object.freeze({ inPreset: true, public: false }),
common: Object.freeze({ inPreset: true, public: false }),
ops: Object.freeze({ inPreset: true, public: false }),
topology: Object.freeze({ inPreset: false, public: false }),
})
/**
* Model-facing tool names (`netx__*`) owned by each group.
* nms tools use generic `Nms` names; HTTP still hits the configured NMS provider adapter.
* NMS tools use generic `Nms` names; HTTP still hits the configured NMS provider adapter.
*/
export const TOOLS_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, readonly string[]>> = Object.freeze({
nms: Object.freeze([
ops: Object.freeze([
'netx__queryNmsAlarms',
'netx__aggregateNmsAlarms',
'netx__runNmsDiagnostics',
@ -56,8 +52,6 @@ export const TOOLS_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, readonly str
'netx__aggregateNmsAlarmsRaw',
'netx__listNmsAlarmFields',
'netx__sqlQueryNms',
]),
common: Object.freeze([
'netx__listManagedNe',
'netx__getManagedNe',
'netx__execManagedNe',
@ -86,24 +80,29 @@ export const TOOLS_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, readonly str
/** Skill directory name under skills root / `presets/netxops/skills/<group>/`. */
export const SKILL_DIR_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, string>> = Object.freeze({
nms: 'nms',
common: 'common',
ops: 'ops',
topology: 'topology',
})
export const CAPABILITY_GROUP_IDS: readonly NetxCapabilityGroupId[] = Object.freeze([
'nms',
'common',
'ops',
'topology',
])
/**
* Build group policy from flat settings / Config fields.
* Accepts legacy `groupManagedNe*` as aliases of `groupCommon*`.
* Legacy `groupTopologyLayout*` ORs into `topology` (layout tools now live in topology).
*
* Legacy aliases (all map into `ops`):
* - `groupNms*` / `groupCommon*` / `groupManagedNe*`
*
* Legacy `groupTopologyLayout*` ORs into `topology`.
*/
export function capabilityGroupsFromSettings(
fields: Partial<NetxCapabilityGroupSettingsFields & {
groupNmsInPreset?: boolean
groupNmsPublic?: boolean
groupCommonInPreset?: boolean
groupCommonPublic?: boolean
groupManagedNeInPreset?: boolean
groupManagedNePublic?: boolean
groupTopologyLayoutInPreset?: boolean
@ -111,19 +110,29 @@ export function capabilityGroupsFromSettings(
}> | null | undefined,
): NetxCapabilityGroups {
const src = fields ?? {}
const commonInPreset = src.groupCommonInPreset !== undefined
? src.groupCommonInPreset !== false
: src.groupManagedNeInPreset !== false
const commonPublic = src.groupCommonPublic === true
|| src.groupManagedNePublic === true
let opsInPreset = true
if (src.groupOpsInPreset !== undefined) {
opsInPreset = src.groupOpsInPreset !== false
} else {
const legacy = [
src.groupNmsInPreset,
src.groupCommonInPreset,
src.groupManagedNeInPreset,
].filter((v): v is boolean => v !== undefined)
if (legacy.length > 0) {
// Any explicit true → on; all explicit false → off.
opsInPreset = legacy.some((v) => v === true)
}
}
return {
nms: {
inPreset: src.groupNmsInPreset !== false,
public: src.groupNmsPublic === true,
},
common: {
inPreset: commonInPreset,
public: commonPublic,
ops: {
inPreset: opsInPreset,
public: src.groupOpsPublic === true
|| src.groupNmsPublic === true
|| src.groupCommonPublic === true
|| src.groupManagedNePublic === true,
},
topology: {
inPreset: src.groupTopologyInPreset === true
@ -136,9 +145,6 @@ export function capabilityGroupsFromSettings(
/**
* Groups enabled for one registration plane.
* @param groups - published policy.
* @param plane - preset scope vs host public layer.
* @param only - when set, intersect with this allow-list (per-export tools packages).
*/
export function groupsForPlane(
groups: NetxCapabilityGroups | undefined,
@ -155,13 +161,16 @@ export function groupsForPlane(
}
/**
* Force-enable the listed groups (for `dsh-netxops/tools-<group>` mounts that
* intentionally ignore Ops-preset inPreset flags).
* Force-enable the listed groups (for `dsh-netxops/tools-<group>` mounts).
* Legacy `nms` / `common` aliases resolve to `ops`.
*/
export function groupsForced(
only: readonly NetxCapabilityGroupId[],
only: readonly (NetxCapabilityGroupId | 'nms' | 'common')[],
): NetxCapabilityGroupId[] {
return CAPABILITY_GROUP_IDS.filter((id) => only.includes(id))
const normalized = only.map((id) => (
id === 'nms' || id === 'common' ? 'ops' as const : id
))
return CAPABILITY_GROUP_IDS.filter((id) => normalized.includes(id))
}
/**

View file

@ -15,34 +15,32 @@ import {
TOOLS_BY_GROUP,
} from '../src/netx/capability-groups.ts'
test('defaults: nms + common in preset; topology and public off', () => {
test('defaults: ops in preset; topology and public off', () => {
assert.deepEqual(DEFAULT_CAPABILITY_GROUPS, {
nms: { inPreset: true, public: false },
common: { inPreset: true, public: false },
ops: { inPreset: true, public: false },
topology: { inPreset: false, public: false },
})
assert.deepEqual(groupsForPlane(undefined, 'preset'), ['nms', 'common'])
assert.deepEqual(groupsForPlane(undefined, 'preset'), ['ops'])
assert.deepEqual(groupsForPlane(undefined, 'public'), [])
})
test('one group one skill dir; topology owns canvas + layout tools', () => {
test('one group one skill dir; ops owns NMS + managed CLI; topology owns canvas', () => {
assert.deepEqual(SKILL_DIR_BY_GROUP, {
nms: 'nms',
common: 'common',
ops: 'ops',
topology: 'topology',
})
assert.ok(TOOLS_BY_GROUP.common.includes('netx__findTopologyPaths'))
assert.ok(TOOLS_BY_GROUP.ops.includes('netx__findTopologyPaths'))
assert.ok(TOOLS_BY_GROUP.ops.includes('netx__execManagedNe'))
assert.ok(TOOLS_BY_GROUP.ops.includes('netx__queryNmsAlarms'))
assert.ok(TOOLS_BY_GROUP.topology.includes('netx__getTopologyTree'))
assert.ok(TOOLS_BY_GROUP.topology.includes('netx__layoutTopologyView'))
assert.ok(TOOLS_BY_GROUP.topology.includes('netx__suggestSinkHubs'))
assert.ok(TOOLS_BY_GROUP.topology.includes('netx__sinkTopologyDualUnits'))
assert.ok(TOOLS_BY_GROUP.nms.includes('netx__queryNmsAlarms'))
})
test('capabilityGroupsFromSettings honors explicit false for inPreset defaults', () => {
const groups = capabilityGroupsFromSettings({
groupNmsInPreset: false,
groupCommonInPreset: false,
groupOpsInPreset: false,
groupTopologyInPreset: true,
groupTopologyPublic: true,
})
@ -53,18 +51,27 @@ test('capabilityGroupsFromSettings honors explicit false for inPreset defaults',
assert.equal(toolNamesForGroups(['topology']).has('netx__queryNmsAlarms'), false)
})
test('legacy groupManagedNe* maps to common; legacy layout flags OR into topology', () => {
test('legacy groupNms/common/managedNe map to ops; legacy layout flags OR into topology', () => {
const groups = capabilityGroupsFromSettings({
groupManagedNeInPreset: false,
groupNmsInPreset: false,
groupCommonInPreset: false,
groupManagedNePublic: true,
groupTopologyLayoutInPreset: true,
})
assert.equal(groups.common.inPreset, false)
assert.equal(groups.common.public, true)
assert.equal(groups.ops.inPreset, false)
assert.equal(groups.ops.public, true)
assert.equal(groups.topology.inPreset, true)
})
test('groupsForced ignores settings for per-export mounts', () => {
assert.deepEqual(groupsForced(['common']), ['common'])
assert.deepEqual(groupsForced(['nms', 'topology']), ['nms', 'topology'])
test('legacy any-true turns ops inPreset on', () => {
const groups = capabilityGroupsFromSettings({
groupNmsInPreset: false,
groupCommonInPreset: true,
})
assert.equal(groups.ops.inPreset, true)
})
test('groupsForced ignores settings for per-export mounts', () => {
assert.deepEqual(groupsForced(['ops']), ['ops'])
assert.deepEqual(groupsForced(['ops', 'topology']), ['ops', 'topology'])
})

View file

@ -10,7 +10,7 @@ import { join } from 'node:path'
import { opsSkillsRoot } from '../src/netx/group-skills.ts'
import { SKILL_DIR_BY_GROUP } from '../src/netx/capability-groups.ts'
test('opsSkillsRoot points at nms / common / topology playbook dirs', () => {
test('opsSkillsRoot points at ops / topology playbook dirs', () => {
const root = opsSkillsRoot()
assert.ok(existsSync(root), root)
for (const dir of Object.values(SKILL_DIR_BY_GROUP)) {