mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-11 05:50:44 +08:00
Add listBizMonitors catalog tool so agents can discover projects/tasks without an id.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5a2ab85d14
commit
c1d07f7509
12 changed files with 984 additions and 19 deletions
|
|
@ -27,6 +27,7 @@ Host tools call netx REST directly (**not** netx-mcp). Token needs `biz-monitor:
|
||||||
|
|
||||||
| Tool | Role |
|
| Tool | Role |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| `listBizMonitors` | Catalog: cutover projects + biz_state tasks (no id required) |
|
||||||
| `getBizMonitorContext` | Project + templates + normalize + port map + tasks |
|
| `getBizMonitorContext` | Project + templates + normalize + port map + tasks |
|
||||||
| `getBizMonitorBoard` | Batch / evaluate progress |
|
| `getBizMonitorBoard` | Batch / evaluate progress |
|
||||||
| `listBizMonitorReds` / `getBizMonitorDiffs` | Reds & diffs with evidence (raw A/B + show cmd) |
|
| `listBizMonitorReds` / `getBizMonitorDiffs` | Reds & diffs with evidence (raw A/B + show cmd) |
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ var TOOLS_BY_GROUP = Object.freeze({
|
||||||
"netx__sinkTopologyDualUnits"
|
"netx__sinkTopologyDualUnits"
|
||||||
]),
|
]),
|
||||||
bizMonitor: Object.freeze([
|
bizMonitor: Object.freeze([
|
||||||
|
"netx__listBizMonitors",
|
||||||
"netx__getBizMonitorContext",
|
"netx__getBizMonitorContext",
|
||||||
"netx__getBizMonitorBoard",
|
"netx__getBizMonitorBoard",
|
||||||
"netx__listBizMonitorReds",
|
"netx__listBizMonitorReds",
|
||||||
|
|
@ -1469,6 +1470,116 @@ async function findTopologyPaths(client, args, signal) {
|
||||||
body.to_managed_ne_id = toMid;
|
body.to_managed_ne_id = toMid;
|
||||||
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
||||||
}
|
}
|
||||||
|
function taskBriefNe(brief) {
|
||||||
|
const rec = asRecord(brief);
|
||||||
|
const name = typeof rec.ne_name === "string" ? rec.ne_name.trim() : "";
|
||||||
|
const ip = typeof rec.ne_ip === "string" ? rec.ne_ip.trim() : "";
|
||||||
|
if (name && ip)
|
||||||
|
return `${name} (${ip})`;
|
||||||
|
return name || ip;
|
||||||
|
}
|
||||||
|
function slimMigrationProject(row) {
|
||||||
|
const mt = asRecord(row.monitor_template);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
note: row.note,
|
||||||
|
monitor_template_id: row.monitor_template_id,
|
||||||
|
monitor_template_name: typeof mt.name === "string" ? mt.name : "",
|
||||||
|
old_task_id: row.old_task_id,
|
||||||
|
new_task_id: row.new_task_id,
|
||||||
|
old_hf_task_id: row.old_hf_task_id,
|
||||||
|
new_hf_task_id: row.new_hf_task_id,
|
||||||
|
old_ne: taskBriefNe(row.old_task),
|
||||||
|
new_ne: taskBriefNe(row.new_task),
|
||||||
|
hf_interval_sec: row.hf_interval_sec,
|
||||||
|
hf_start_at: row.hf_start_at,
|
||||||
|
hf_end_at: row.hf_end_at,
|
||||||
|
updated_at: row.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function matchesBizMonitorQ(row, q) {
|
||||||
|
if (!q)
|
||||||
|
return true;
|
||||||
|
const hay = [
|
||||||
|
row.id,
|
||||||
|
row.name,
|
||||||
|
row.note,
|
||||||
|
row.status,
|
||||||
|
row.ne_name,
|
||||||
|
row.ne_ip,
|
||||||
|
row.ne_id,
|
||||||
|
row.old_ne,
|
||||||
|
row.new_ne,
|
||||||
|
row.monitor_template_name,
|
||||||
|
row.purpose,
|
||||||
|
row.vendor
|
||||||
|
].map((v) => typeof v === "string" || typeof v === "number" ? String(v).toLowerCase() : "").join(" ");
|
||||||
|
return hay.includes(q);
|
||||||
|
}
|
||||||
|
async function listBizMonitors(client, args, signal) {
|
||||||
|
const kindRaw = str2(args, "kind", "all").trim().toLowerCase() || "all";
|
||||||
|
const kind = kindRaw === "project" ? "projects" : kindRaw === "task" ? "tasks" : kindRaw;
|
||||||
|
if (kind !== "all" && kind !== "projects" && kind !== "tasks") {
|
||||||
|
return { ok: false, error: "kind_invalid", detail: "kind must be all|projects|tasks" };
|
||||||
|
}
|
||||||
|
const purpose = str2(args, "purpose").trim();
|
||||||
|
const status = str2(args, "status").trim().toLowerCase();
|
||||||
|
const q = str2(args, "q").trim().toLowerCase();
|
||||||
|
const limit = clampInt(num2(args, "limit") ?? 100, 100, 1, 500);
|
||||||
|
const wantProjects = kind === "all" || kind === "projects";
|
||||||
|
const wantTasks = kind === "all" || kind === "tasks";
|
||||||
|
const fetches = [];
|
||||||
|
if (wantProjects)
|
||||||
|
fetches.push(client.get("/v1/biz-migration/projects", {}, signal));
|
||||||
|
else
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
if (wantTasks) {
|
||||||
|
const params = {};
|
||||||
|
if (purpose)
|
||||||
|
params.purpose = purpose;
|
||||||
|
fetches.push(client.get("/v1/biz-state/tasks", params, signal));
|
||||||
|
} else {
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
}
|
||||||
|
const [projRes, taskRes] = await Promise.all(fetches);
|
||||||
|
if (wantProjects && projRes.ok === false)
|
||||||
|
return projRes;
|
||||||
|
if (wantTasks && taskRes.ok === false)
|
||||||
|
return taskRes;
|
||||||
|
const projItems = Array.isArray(asRecord(projRes.data).items) ? asRecord(projRes.data).items : [];
|
||||||
|
const taskItems = Array.isArray(asRecord(taskRes.data).items) ? asRecord(taskRes.data).items : [];
|
||||||
|
let projects = projItems.map((row) => slimMigrationProject(asRecord(row))).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
let tasks = taskItems.map((row) => asRecord(row)).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
const projectsTotal = projects.length;
|
||||||
|
const tasksTotal = tasks.length;
|
||||||
|
projects = projects.slice(0, limit);
|
||||||
|
tasks = tasks.slice(0, limit);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
kind,
|
||||||
|
projects: wantProjects ? projects : undefined,
|
||||||
|
tasks: wantTasks ? tasks : undefined,
|
||||||
|
counts: {
|
||||||
|
projects: wantProjects ? projectsTotal : 0,
|
||||||
|
tasks: wantTasks ? tasksTotal : 0,
|
||||||
|
projects_returned: wantProjects ? projects.length : 0,
|
||||||
|
tasks_returned: wantTasks ? tasks.length : 0
|
||||||
|
},
|
||||||
|
next: "Pass project_id (or task_id) to netx__getBizMonitorContext for the full definition bundle."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
async function getBizMonitorContext(client, args, signal) {
|
async function getBizMonitorContext(client, args, signal) {
|
||||||
const projectId = str2(args, "project_id").trim();
|
const projectId = str2(args, "project_id").trim();
|
||||||
const taskId = str2(args, "task_id").trim();
|
const taskId = str2(args, "task_id").trim();
|
||||||
|
|
@ -2355,7 +2466,14 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
layer: str4(),
|
layer: str4(),
|
||||||
detail: str4("summary | full")
|
detail: str4("summary | full")
|
||||||
}, findTopologyPaths, getClient, t),
|
}, findTopologyPaths, getClient, t),
|
||||||
tool2("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.", {
|
tool2("netx__listBizMonitors", "Catalog: list cutover/monitor-compare projects and biz_state monitor tasks. No project_id needed — call this first when the user did not give an id, then use getBizMonitorContext.", {
|
||||||
|
kind: str4("all | projects | tasks (default all)"),
|
||||||
|
purpose: str4("Filter tasks: portrait | cutover_hf | empty=all"),
|
||||||
|
status: str4("Filter by status (e.g. active / paused)"),
|
||||||
|
q: str4("Keyword on name / ne_name / note / id"),
|
||||||
|
limit: num4("Max rows per list (default 100, max 500)")
|
||||||
|
}, listBizMonitors, getClient, t),
|
||||||
|
tool2("netx__getBizMonitorContext", "Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id from listBizMonitors. Display keys are device-raw A/B.", {
|
||||||
project_id: str4("Cutover / biz-migration project id"),
|
project_id: str4("Cutover / biz-migration project id"),
|
||||||
task_id: str4("biz_state task id (optional if project_id set)")
|
task_id: str4("biz_state task id (optional if project_id set)")
|
||||||
}, getBizMonitorContext, getClient, t),
|
}, getBizMonitorContext, getClient, t),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ var TOOLS_BY_GROUP = Object.freeze({
|
||||||
"netx__sinkTopologyDualUnits"
|
"netx__sinkTopologyDualUnits"
|
||||||
]),
|
]),
|
||||||
bizMonitor: Object.freeze([
|
bizMonitor: Object.freeze([
|
||||||
|
"netx__listBizMonitors",
|
||||||
"netx__getBizMonitorContext",
|
"netx__getBizMonitorContext",
|
||||||
"netx__getBizMonitorBoard",
|
"netx__getBizMonitorBoard",
|
||||||
"netx__listBizMonitorReds",
|
"netx__listBizMonitorReds",
|
||||||
|
|
@ -1469,6 +1470,116 @@ async function findTopologyPaths(client, args, signal) {
|
||||||
body.to_managed_ne_id = toMid;
|
body.to_managed_ne_id = toMid;
|
||||||
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
||||||
}
|
}
|
||||||
|
function taskBriefNe(brief) {
|
||||||
|
const rec = asRecord(brief);
|
||||||
|
const name = typeof rec.ne_name === "string" ? rec.ne_name.trim() : "";
|
||||||
|
const ip = typeof rec.ne_ip === "string" ? rec.ne_ip.trim() : "";
|
||||||
|
if (name && ip)
|
||||||
|
return `${name} (${ip})`;
|
||||||
|
return name || ip;
|
||||||
|
}
|
||||||
|
function slimMigrationProject(row) {
|
||||||
|
const mt = asRecord(row.monitor_template);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
note: row.note,
|
||||||
|
monitor_template_id: row.monitor_template_id,
|
||||||
|
monitor_template_name: typeof mt.name === "string" ? mt.name : "",
|
||||||
|
old_task_id: row.old_task_id,
|
||||||
|
new_task_id: row.new_task_id,
|
||||||
|
old_hf_task_id: row.old_hf_task_id,
|
||||||
|
new_hf_task_id: row.new_hf_task_id,
|
||||||
|
old_ne: taskBriefNe(row.old_task),
|
||||||
|
new_ne: taskBriefNe(row.new_task),
|
||||||
|
hf_interval_sec: row.hf_interval_sec,
|
||||||
|
hf_start_at: row.hf_start_at,
|
||||||
|
hf_end_at: row.hf_end_at,
|
||||||
|
updated_at: row.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function matchesBizMonitorQ(row, q) {
|
||||||
|
if (!q)
|
||||||
|
return true;
|
||||||
|
const hay = [
|
||||||
|
row.id,
|
||||||
|
row.name,
|
||||||
|
row.note,
|
||||||
|
row.status,
|
||||||
|
row.ne_name,
|
||||||
|
row.ne_ip,
|
||||||
|
row.ne_id,
|
||||||
|
row.old_ne,
|
||||||
|
row.new_ne,
|
||||||
|
row.monitor_template_name,
|
||||||
|
row.purpose,
|
||||||
|
row.vendor
|
||||||
|
].map((v) => typeof v === "string" || typeof v === "number" ? String(v).toLowerCase() : "").join(" ");
|
||||||
|
return hay.includes(q);
|
||||||
|
}
|
||||||
|
async function listBizMonitors(client, args, signal) {
|
||||||
|
const kindRaw = str2(args, "kind", "all").trim().toLowerCase() || "all";
|
||||||
|
const kind = kindRaw === "project" ? "projects" : kindRaw === "task" ? "tasks" : kindRaw;
|
||||||
|
if (kind !== "all" && kind !== "projects" && kind !== "tasks") {
|
||||||
|
return { ok: false, error: "kind_invalid", detail: "kind must be all|projects|tasks" };
|
||||||
|
}
|
||||||
|
const purpose = str2(args, "purpose").trim();
|
||||||
|
const status = str2(args, "status").trim().toLowerCase();
|
||||||
|
const q = str2(args, "q").trim().toLowerCase();
|
||||||
|
const limit = clampInt(num2(args, "limit") ?? 100, 100, 1, 500);
|
||||||
|
const wantProjects = kind === "all" || kind === "projects";
|
||||||
|
const wantTasks = kind === "all" || kind === "tasks";
|
||||||
|
const fetches = [];
|
||||||
|
if (wantProjects)
|
||||||
|
fetches.push(client.get("/v1/biz-migration/projects", {}, signal));
|
||||||
|
else
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
if (wantTasks) {
|
||||||
|
const params = {};
|
||||||
|
if (purpose)
|
||||||
|
params.purpose = purpose;
|
||||||
|
fetches.push(client.get("/v1/biz-state/tasks", params, signal));
|
||||||
|
} else {
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
}
|
||||||
|
const [projRes, taskRes] = await Promise.all(fetches);
|
||||||
|
if (wantProjects && projRes.ok === false)
|
||||||
|
return projRes;
|
||||||
|
if (wantTasks && taskRes.ok === false)
|
||||||
|
return taskRes;
|
||||||
|
const projItems = Array.isArray(asRecord(projRes.data).items) ? asRecord(projRes.data).items : [];
|
||||||
|
const taskItems = Array.isArray(asRecord(taskRes.data).items) ? asRecord(taskRes.data).items : [];
|
||||||
|
let projects = projItems.map((row) => slimMigrationProject(asRecord(row))).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
let tasks = taskItems.map((row) => asRecord(row)).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
const projectsTotal = projects.length;
|
||||||
|
const tasksTotal = tasks.length;
|
||||||
|
projects = projects.slice(0, limit);
|
||||||
|
tasks = tasks.slice(0, limit);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
kind,
|
||||||
|
projects: wantProjects ? projects : undefined,
|
||||||
|
tasks: wantTasks ? tasks : undefined,
|
||||||
|
counts: {
|
||||||
|
projects: wantProjects ? projectsTotal : 0,
|
||||||
|
tasks: wantTasks ? tasksTotal : 0,
|
||||||
|
projects_returned: wantProjects ? projects.length : 0,
|
||||||
|
tasks_returned: wantTasks ? tasks.length : 0
|
||||||
|
},
|
||||||
|
next: "Pass project_id (or task_id) to netx__getBizMonitorContext for the full definition bundle."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
async function getBizMonitorContext(client, args, signal) {
|
async function getBizMonitorContext(client, args, signal) {
|
||||||
const projectId = str2(args, "project_id").trim();
|
const projectId = str2(args, "project_id").trim();
|
||||||
const taskId = str2(args, "task_id").trim();
|
const taskId = str2(args, "task_id").trim();
|
||||||
|
|
@ -2355,7 +2466,14 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
layer: str4(),
|
layer: str4(),
|
||||||
detail: str4("summary | full")
|
detail: str4("summary | full")
|
||||||
}, findTopologyPaths, getClient, t),
|
}, findTopologyPaths, getClient, t),
|
||||||
tool2("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.", {
|
tool2("netx__listBizMonitors", "Catalog: list cutover/monitor-compare projects and biz_state monitor tasks. No project_id needed — call this first when the user did not give an id, then use getBizMonitorContext.", {
|
||||||
|
kind: str4("all | projects | tasks (default all)"),
|
||||||
|
purpose: str4("Filter tasks: portrait | cutover_hf | empty=all"),
|
||||||
|
status: str4("Filter by status (e.g. active / paused)"),
|
||||||
|
q: str4("Keyword on name / ne_name / note / id"),
|
||||||
|
limit: num4("Max rows per list (default 100, max 500)")
|
||||||
|
}, listBizMonitors, getClient, t),
|
||||||
|
tool2("netx__getBizMonitorContext", "Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id from listBizMonitors. Display keys are device-raw A/B.", {
|
||||||
project_id: str4("Cutover / biz-migration project id"),
|
project_id: str4("Cutover / biz-migration project id"),
|
||||||
task_id: str4("biz_state task id (optional if project_id set)")
|
task_id: str4("biz_state task id (optional if project_id set)")
|
||||||
}, getBizMonitorContext, getClient, t),
|
}, getBizMonitorContext, getClient, t),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ var TOOLS_BY_GROUP = Object.freeze({
|
||||||
"netx__sinkTopologyDualUnits"
|
"netx__sinkTopologyDualUnits"
|
||||||
]),
|
]),
|
||||||
bizMonitor: Object.freeze([
|
bizMonitor: Object.freeze([
|
||||||
|
"netx__listBizMonitors",
|
||||||
"netx__getBizMonitorContext",
|
"netx__getBizMonitorContext",
|
||||||
"netx__getBizMonitorBoard",
|
"netx__getBizMonitorBoard",
|
||||||
"netx__listBizMonitorReds",
|
"netx__listBizMonitorReds",
|
||||||
|
|
@ -1469,6 +1470,116 @@ async function findTopologyPaths(client, args, signal) {
|
||||||
body.to_managed_ne_id = toMid;
|
body.to_managed_ne_id = toMid;
|
||||||
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
||||||
}
|
}
|
||||||
|
function taskBriefNe(brief) {
|
||||||
|
const rec = asRecord(brief);
|
||||||
|
const name = typeof rec.ne_name === "string" ? rec.ne_name.trim() : "";
|
||||||
|
const ip = typeof rec.ne_ip === "string" ? rec.ne_ip.trim() : "";
|
||||||
|
if (name && ip)
|
||||||
|
return `${name} (${ip})`;
|
||||||
|
return name || ip;
|
||||||
|
}
|
||||||
|
function slimMigrationProject(row) {
|
||||||
|
const mt = asRecord(row.monitor_template);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
note: row.note,
|
||||||
|
monitor_template_id: row.monitor_template_id,
|
||||||
|
monitor_template_name: typeof mt.name === "string" ? mt.name : "",
|
||||||
|
old_task_id: row.old_task_id,
|
||||||
|
new_task_id: row.new_task_id,
|
||||||
|
old_hf_task_id: row.old_hf_task_id,
|
||||||
|
new_hf_task_id: row.new_hf_task_id,
|
||||||
|
old_ne: taskBriefNe(row.old_task),
|
||||||
|
new_ne: taskBriefNe(row.new_task),
|
||||||
|
hf_interval_sec: row.hf_interval_sec,
|
||||||
|
hf_start_at: row.hf_start_at,
|
||||||
|
hf_end_at: row.hf_end_at,
|
||||||
|
updated_at: row.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function matchesBizMonitorQ(row, q) {
|
||||||
|
if (!q)
|
||||||
|
return true;
|
||||||
|
const hay = [
|
||||||
|
row.id,
|
||||||
|
row.name,
|
||||||
|
row.note,
|
||||||
|
row.status,
|
||||||
|
row.ne_name,
|
||||||
|
row.ne_ip,
|
||||||
|
row.ne_id,
|
||||||
|
row.old_ne,
|
||||||
|
row.new_ne,
|
||||||
|
row.monitor_template_name,
|
||||||
|
row.purpose,
|
||||||
|
row.vendor
|
||||||
|
].map((v) => typeof v === "string" || typeof v === "number" ? String(v).toLowerCase() : "").join(" ");
|
||||||
|
return hay.includes(q);
|
||||||
|
}
|
||||||
|
async function listBizMonitors(client, args, signal) {
|
||||||
|
const kindRaw = str2(args, "kind", "all").trim().toLowerCase() || "all";
|
||||||
|
const kind = kindRaw === "project" ? "projects" : kindRaw === "task" ? "tasks" : kindRaw;
|
||||||
|
if (kind !== "all" && kind !== "projects" && kind !== "tasks") {
|
||||||
|
return { ok: false, error: "kind_invalid", detail: "kind must be all|projects|tasks" };
|
||||||
|
}
|
||||||
|
const purpose = str2(args, "purpose").trim();
|
||||||
|
const status = str2(args, "status").trim().toLowerCase();
|
||||||
|
const q = str2(args, "q").trim().toLowerCase();
|
||||||
|
const limit = clampInt(num2(args, "limit") ?? 100, 100, 1, 500);
|
||||||
|
const wantProjects = kind === "all" || kind === "projects";
|
||||||
|
const wantTasks = kind === "all" || kind === "tasks";
|
||||||
|
const fetches = [];
|
||||||
|
if (wantProjects)
|
||||||
|
fetches.push(client.get("/v1/biz-migration/projects", {}, signal));
|
||||||
|
else
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
if (wantTasks) {
|
||||||
|
const params = {};
|
||||||
|
if (purpose)
|
||||||
|
params.purpose = purpose;
|
||||||
|
fetches.push(client.get("/v1/biz-state/tasks", params, signal));
|
||||||
|
} else {
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
}
|
||||||
|
const [projRes, taskRes] = await Promise.all(fetches);
|
||||||
|
if (wantProjects && projRes.ok === false)
|
||||||
|
return projRes;
|
||||||
|
if (wantTasks && taskRes.ok === false)
|
||||||
|
return taskRes;
|
||||||
|
const projItems = Array.isArray(asRecord(projRes.data).items) ? asRecord(projRes.data).items : [];
|
||||||
|
const taskItems = Array.isArray(asRecord(taskRes.data).items) ? asRecord(taskRes.data).items : [];
|
||||||
|
let projects = projItems.map((row) => slimMigrationProject(asRecord(row))).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
let tasks = taskItems.map((row) => asRecord(row)).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
const projectsTotal = projects.length;
|
||||||
|
const tasksTotal = tasks.length;
|
||||||
|
projects = projects.slice(0, limit);
|
||||||
|
tasks = tasks.slice(0, limit);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
kind,
|
||||||
|
projects: wantProjects ? projects : undefined,
|
||||||
|
tasks: wantTasks ? tasks : undefined,
|
||||||
|
counts: {
|
||||||
|
projects: wantProjects ? projectsTotal : 0,
|
||||||
|
tasks: wantTasks ? tasksTotal : 0,
|
||||||
|
projects_returned: wantProjects ? projects.length : 0,
|
||||||
|
tasks_returned: wantTasks ? tasks.length : 0
|
||||||
|
},
|
||||||
|
next: "Pass project_id (or task_id) to netx__getBizMonitorContext for the full definition bundle."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
async function getBizMonitorContext(client, args, signal) {
|
async function getBizMonitorContext(client, args, signal) {
|
||||||
const projectId = str2(args, "project_id").trim();
|
const projectId = str2(args, "project_id").trim();
|
||||||
const taskId = str2(args, "task_id").trim();
|
const taskId = str2(args, "task_id").trim();
|
||||||
|
|
@ -2355,7 +2466,14 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
layer: str4(),
|
layer: str4(),
|
||||||
detail: str4("summary | full")
|
detail: str4("summary | full")
|
||||||
}, findTopologyPaths, getClient, t),
|
}, findTopologyPaths, getClient, t),
|
||||||
tool2("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.", {
|
tool2("netx__listBizMonitors", "Catalog: list cutover/monitor-compare projects and biz_state monitor tasks. No project_id needed — call this first when the user did not give an id, then use getBizMonitorContext.", {
|
||||||
|
kind: str4("all | projects | tasks (default all)"),
|
||||||
|
purpose: str4("Filter tasks: portrait | cutover_hf | empty=all"),
|
||||||
|
status: str4("Filter by status (e.g. active / paused)"),
|
||||||
|
q: str4("Keyword on name / ne_name / note / id"),
|
||||||
|
limit: num4("Max rows per list (default 100, max 500)")
|
||||||
|
}, listBizMonitors, getClient, t),
|
||||||
|
tool2("netx__getBizMonitorContext", "Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id from listBizMonitors. Display keys are device-raw A/B.", {
|
||||||
project_id: str4("Cutover / biz-migration project id"),
|
project_id: str4("Cutover / biz-migration project id"),
|
||||||
task_id: str4("biz_state task id (optional if project_id set)")
|
task_id: str4("biz_state task id (optional if project_id set)")
|
||||||
}, getBizMonitorContext, getClient, t),
|
}, getBizMonitorContext, getClient, t),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ var TOOLS_BY_GROUP = Object.freeze({
|
||||||
"netx__sinkTopologyDualUnits"
|
"netx__sinkTopologyDualUnits"
|
||||||
]),
|
]),
|
||||||
bizMonitor: Object.freeze([
|
bizMonitor: Object.freeze([
|
||||||
|
"netx__listBizMonitors",
|
||||||
"netx__getBizMonitorContext",
|
"netx__getBizMonitorContext",
|
||||||
"netx__getBizMonitorBoard",
|
"netx__getBizMonitorBoard",
|
||||||
"netx__listBizMonitorReds",
|
"netx__listBizMonitorReds",
|
||||||
|
|
@ -1469,6 +1470,116 @@ async function findTopologyPaths(client, args, signal) {
|
||||||
body.to_managed_ne_id = toMid;
|
body.to_managed_ne_id = toMid;
|
||||||
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
||||||
}
|
}
|
||||||
|
function taskBriefNe(brief) {
|
||||||
|
const rec = asRecord(brief);
|
||||||
|
const name = typeof rec.ne_name === "string" ? rec.ne_name.trim() : "";
|
||||||
|
const ip = typeof rec.ne_ip === "string" ? rec.ne_ip.trim() : "";
|
||||||
|
if (name && ip)
|
||||||
|
return `${name} (${ip})`;
|
||||||
|
return name || ip;
|
||||||
|
}
|
||||||
|
function slimMigrationProject(row) {
|
||||||
|
const mt = asRecord(row.monitor_template);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
note: row.note,
|
||||||
|
monitor_template_id: row.monitor_template_id,
|
||||||
|
monitor_template_name: typeof mt.name === "string" ? mt.name : "",
|
||||||
|
old_task_id: row.old_task_id,
|
||||||
|
new_task_id: row.new_task_id,
|
||||||
|
old_hf_task_id: row.old_hf_task_id,
|
||||||
|
new_hf_task_id: row.new_hf_task_id,
|
||||||
|
old_ne: taskBriefNe(row.old_task),
|
||||||
|
new_ne: taskBriefNe(row.new_task),
|
||||||
|
hf_interval_sec: row.hf_interval_sec,
|
||||||
|
hf_start_at: row.hf_start_at,
|
||||||
|
hf_end_at: row.hf_end_at,
|
||||||
|
updated_at: row.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function matchesBizMonitorQ(row, q) {
|
||||||
|
if (!q)
|
||||||
|
return true;
|
||||||
|
const hay = [
|
||||||
|
row.id,
|
||||||
|
row.name,
|
||||||
|
row.note,
|
||||||
|
row.status,
|
||||||
|
row.ne_name,
|
||||||
|
row.ne_ip,
|
||||||
|
row.ne_id,
|
||||||
|
row.old_ne,
|
||||||
|
row.new_ne,
|
||||||
|
row.monitor_template_name,
|
||||||
|
row.purpose,
|
||||||
|
row.vendor
|
||||||
|
].map((v) => typeof v === "string" || typeof v === "number" ? String(v).toLowerCase() : "").join(" ");
|
||||||
|
return hay.includes(q);
|
||||||
|
}
|
||||||
|
async function listBizMonitors(client, args, signal) {
|
||||||
|
const kindRaw = str2(args, "kind", "all").trim().toLowerCase() || "all";
|
||||||
|
const kind = kindRaw === "project" ? "projects" : kindRaw === "task" ? "tasks" : kindRaw;
|
||||||
|
if (kind !== "all" && kind !== "projects" && kind !== "tasks") {
|
||||||
|
return { ok: false, error: "kind_invalid", detail: "kind must be all|projects|tasks" };
|
||||||
|
}
|
||||||
|
const purpose = str2(args, "purpose").trim();
|
||||||
|
const status = str2(args, "status").trim().toLowerCase();
|
||||||
|
const q = str2(args, "q").trim().toLowerCase();
|
||||||
|
const limit = clampInt(num2(args, "limit") ?? 100, 100, 1, 500);
|
||||||
|
const wantProjects = kind === "all" || kind === "projects";
|
||||||
|
const wantTasks = kind === "all" || kind === "tasks";
|
||||||
|
const fetches = [];
|
||||||
|
if (wantProjects)
|
||||||
|
fetches.push(client.get("/v1/biz-migration/projects", {}, signal));
|
||||||
|
else
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
if (wantTasks) {
|
||||||
|
const params = {};
|
||||||
|
if (purpose)
|
||||||
|
params.purpose = purpose;
|
||||||
|
fetches.push(client.get("/v1/biz-state/tasks", params, signal));
|
||||||
|
} else {
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
}
|
||||||
|
const [projRes, taskRes] = await Promise.all(fetches);
|
||||||
|
if (wantProjects && projRes.ok === false)
|
||||||
|
return projRes;
|
||||||
|
if (wantTasks && taskRes.ok === false)
|
||||||
|
return taskRes;
|
||||||
|
const projItems = Array.isArray(asRecord(projRes.data).items) ? asRecord(projRes.data).items : [];
|
||||||
|
const taskItems = Array.isArray(asRecord(taskRes.data).items) ? asRecord(taskRes.data).items : [];
|
||||||
|
let projects = projItems.map((row) => slimMigrationProject(asRecord(row))).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
let tasks = taskItems.map((row) => asRecord(row)).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
const projectsTotal = projects.length;
|
||||||
|
const tasksTotal = tasks.length;
|
||||||
|
projects = projects.slice(0, limit);
|
||||||
|
tasks = tasks.slice(0, limit);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
kind,
|
||||||
|
projects: wantProjects ? projects : undefined,
|
||||||
|
tasks: wantTasks ? tasks : undefined,
|
||||||
|
counts: {
|
||||||
|
projects: wantProjects ? projectsTotal : 0,
|
||||||
|
tasks: wantTasks ? tasksTotal : 0,
|
||||||
|
projects_returned: wantProjects ? projects.length : 0,
|
||||||
|
tasks_returned: wantTasks ? tasks.length : 0
|
||||||
|
},
|
||||||
|
next: "Pass project_id (or task_id) to netx__getBizMonitorContext for the full definition bundle."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
async function getBizMonitorContext(client, args, signal) {
|
async function getBizMonitorContext(client, args, signal) {
|
||||||
const projectId = str2(args, "project_id").trim();
|
const projectId = str2(args, "project_id").trim();
|
||||||
const taskId = str2(args, "task_id").trim();
|
const taskId = str2(args, "task_id").trim();
|
||||||
|
|
@ -2355,7 +2466,14 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
layer: str4(),
|
layer: str4(),
|
||||||
detail: str4("summary | full")
|
detail: str4("summary | full")
|
||||||
}, findTopologyPaths, getClient, t),
|
}, findTopologyPaths, getClient, t),
|
||||||
tool2("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.", {
|
tool2("netx__listBizMonitors", "Catalog: list cutover/monitor-compare projects and biz_state monitor tasks. No project_id needed — call this first when the user did not give an id, then use getBizMonitorContext.", {
|
||||||
|
kind: str4("all | projects | tasks (default all)"),
|
||||||
|
purpose: str4("Filter tasks: portrait | cutover_hf | empty=all"),
|
||||||
|
status: str4("Filter by status (e.g. active / paused)"),
|
||||||
|
q: str4("Keyword on name / ne_name / note / id"),
|
||||||
|
limit: num4("Max rows per list (default 100, max 500)")
|
||||||
|
}, listBizMonitors, getClient, t),
|
||||||
|
tool2("netx__getBizMonitorContext", "Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id from listBizMonitors. Display keys are device-raw A/B.", {
|
||||||
project_id: str4("Cutover / biz-migration project id"),
|
project_id: str4("Cutover / biz-migration project id"),
|
||||||
task_id: str4("biz_state task id (optional if project_id set)")
|
task_id: str4("biz_state task id (optional if project_id set)")
|
||||||
}, getBizMonitorContext, getClient, t),
|
}, getBizMonitorContext, getClient, t),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ var TOOLS_BY_GROUP = Object.freeze({
|
||||||
"netx__sinkTopologyDualUnits"
|
"netx__sinkTopologyDualUnits"
|
||||||
]),
|
]),
|
||||||
bizMonitor: Object.freeze([
|
bizMonitor: Object.freeze([
|
||||||
|
"netx__listBizMonitors",
|
||||||
"netx__getBizMonitorContext",
|
"netx__getBizMonitorContext",
|
||||||
"netx__getBizMonitorBoard",
|
"netx__getBizMonitorBoard",
|
||||||
"netx__listBizMonitorReds",
|
"netx__listBizMonitorReds",
|
||||||
|
|
@ -1469,6 +1470,116 @@ async function findTopologyPaths(client, args, signal) {
|
||||||
body.to_managed_ne_id = toMid;
|
body.to_managed_ne_id = toMid;
|
||||||
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
||||||
}
|
}
|
||||||
|
function taskBriefNe(brief) {
|
||||||
|
const rec = asRecord(brief);
|
||||||
|
const name = typeof rec.ne_name === "string" ? rec.ne_name.trim() : "";
|
||||||
|
const ip = typeof rec.ne_ip === "string" ? rec.ne_ip.trim() : "";
|
||||||
|
if (name && ip)
|
||||||
|
return `${name} (${ip})`;
|
||||||
|
return name || ip;
|
||||||
|
}
|
||||||
|
function slimMigrationProject(row) {
|
||||||
|
const mt = asRecord(row.monitor_template);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
note: row.note,
|
||||||
|
monitor_template_id: row.monitor_template_id,
|
||||||
|
monitor_template_name: typeof mt.name === "string" ? mt.name : "",
|
||||||
|
old_task_id: row.old_task_id,
|
||||||
|
new_task_id: row.new_task_id,
|
||||||
|
old_hf_task_id: row.old_hf_task_id,
|
||||||
|
new_hf_task_id: row.new_hf_task_id,
|
||||||
|
old_ne: taskBriefNe(row.old_task),
|
||||||
|
new_ne: taskBriefNe(row.new_task),
|
||||||
|
hf_interval_sec: row.hf_interval_sec,
|
||||||
|
hf_start_at: row.hf_start_at,
|
||||||
|
hf_end_at: row.hf_end_at,
|
||||||
|
updated_at: row.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function matchesBizMonitorQ(row, q) {
|
||||||
|
if (!q)
|
||||||
|
return true;
|
||||||
|
const hay = [
|
||||||
|
row.id,
|
||||||
|
row.name,
|
||||||
|
row.note,
|
||||||
|
row.status,
|
||||||
|
row.ne_name,
|
||||||
|
row.ne_ip,
|
||||||
|
row.ne_id,
|
||||||
|
row.old_ne,
|
||||||
|
row.new_ne,
|
||||||
|
row.monitor_template_name,
|
||||||
|
row.purpose,
|
||||||
|
row.vendor
|
||||||
|
].map((v) => typeof v === "string" || typeof v === "number" ? String(v).toLowerCase() : "").join(" ");
|
||||||
|
return hay.includes(q);
|
||||||
|
}
|
||||||
|
async function listBizMonitors(client, args, signal) {
|
||||||
|
const kindRaw = str2(args, "kind", "all").trim().toLowerCase() || "all";
|
||||||
|
const kind = kindRaw === "project" ? "projects" : kindRaw === "task" ? "tasks" : kindRaw;
|
||||||
|
if (kind !== "all" && kind !== "projects" && kind !== "tasks") {
|
||||||
|
return { ok: false, error: "kind_invalid", detail: "kind must be all|projects|tasks" };
|
||||||
|
}
|
||||||
|
const purpose = str2(args, "purpose").trim();
|
||||||
|
const status = str2(args, "status").trim().toLowerCase();
|
||||||
|
const q = str2(args, "q").trim().toLowerCase();
|
||||||
|
const limit = clampInt(num2(args, "limit") ?? 100, 100, 1, 500);
|
||||||
|
const wantProjects = kind === "all" || kind === "projects";
|
||||||
|
const wantTasks = kind === "all" || kind === "tasks";
|
||||||
|
const fetches = [];
|
||||||
|
if (wantProjects)
|
||||||
|
fetches.push(client.get("/v1/biz-migration/projects", {}, signal));
|
||||||
|
else
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
if (wantTasks) {
|
||||||
|
const params = {};
|
||||||
|
if (purpose)
|
||||||
|
params.purpose = purpose;
|
||||||
|
fetches.push(client.get("/v1/biz-state/tasks", params, signal));
|
||||||
|
} else {
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
}
|
||||||
|
const [projRes, taskRes] = await Promise.all(fetches);
|
||||||
|
if (wantProjects && projRes.ok === false)
|
||||||
|
return projRes;
|
||||||
|
if (wantTasks && taskRes.ok === false)
|
||||||
|
return taskRes;
|
||||||
|
const projItems = Array.isArray(asRecord(projRes.data).items) ? asRecord(projRes.data).items : [];
|
||||||
|
const taskItems = Array.isArray(asRecord(taskRes.data).items) ? asRecord(taskRes.data).items : [];
|
||||||
|
let projects = projItems.map((row) => slimMigrationProject(asRecord(row))).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
let tasks = taskItems.map((row) => asRecord(row)).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
const projectsTotal = projects.length;
|
||||||
|
const tasksTotal = tasks.length;
|
||||||
|
projects = projects.slice(0, limit);
|
||||||
|
tasks = tasks.slice(0, limit);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
kind,
|
||||||
|
projects: wantProjects ? projects : undefined,
|
||||||
|
tasks: wantTasks ? tasks : undefined,
|
||||||
|
counts: {
|
||||||
|
projects: wantProjects ? projectsTotal : 0,
|
||||||
|
tasks: wantTasks ? tasksTotal : 0,
|
||||||
|
projects_returned: wantProjects ? projects.length : 0,
|
||||||
|
tasks_returned: wantTasks ? tasks.length : 0
|
||||||
|
},
|
||||||
|
next: "Pass project_id (or task_id) to netx__getBizMonitorContext for the full definition bundle."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
async function getBizMonitorContext(client, args, signal) {
|
async function getBizMonitorContext(client, args, signal) {
|
||||||
const projectId = str2(args, "project_id").trim();
|
const projectId = str2(args, "project_id").trim();
|
||||||
const taskId = str2(args, "task_id").trim();
|
const taskId = str2(args, "task_id").trim();
|
||||||
|
|
@ -2355,7 +2466,14 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
layer: str4(),
|
layer: str4(),
|
||||||
detail: str4("summary | full")
|
detail: str4("summary | full")
|
||||||
}, findTopologyPaths, getClient, t),
|
}, findTopologyPaths, getClient, t),
|
||||||
tool2("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.", {
|
tool2("netx__listBizMonitors", "Catalog: list cutover/monitor-compare projects and biz_state monitor tasks. No project_id needed — call this first when the user did not give an id, then use getBizMonitorContext.", {
|
||||||
|
kind: str4("all | projects | tasks (default all)"),
|
||||||
|
purpose: str4("Filter tasks: portrait | cutover_hf | empty=all"),
|
||||||
|
status: str4("Filter by status (e.g. active / paused)"),
|
||||||
|
q: str4("Keyword on name / ne_name / note / id"),
|
||||||
|
limit: num4("Max rows per list (default 100, max 500)")
|
||||||
|
}, listBizMonitors, getClient, t),
|
||||||
|
tool2("netx__getBizMonitorContext", "Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id from listBizMonitors. Display keys are device-raw A/B.", {
|
||||||
project_id: str4("Cutover / biz-migration project id"),
|
project_id: str4("Cutover / biz-migration project id"),
|
||||||
task_id: str4("biz_state task id (optional if project_id set)")
|
task_id: str4("biz_state task id (optional if project_id set)")
|
||||||
}, getBizMonitorContext, getClient, t),
|
}, getBizMonitorContext, getClient, t),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ var TOOLS_BY_GROUP = Object.freeze({
|
||||||
"netx__sinkTopologyDualUnits"
|
"netx__sinkTopologyDualUnits"
|
||||||
]),
|
]),
|
||||||
bizMonitor: Object.freeze([
|
bizMonitor: Object.freeze([
|
||||||
|
"netx__listBizMonitors",
|
||||||
"netx__getBizMonitorContext",
|
"netx__getBizMonitorContext",
|
||||||
"netx__getBizMonitorBoard",
|
"netx__getBizMonitorBoard",
|
||||||
"netx__listBizMonitorReds",
|
"netx__listBizMonitorReds",
|
||||||
|
|
@ -1469,6 +1470,116 @@ async function findTopologyPaths(client, args, signal) {
|
||||||
body.to_managed_ne_id = toMid;
|
body.to_managed_ne_id = toMid;
|
||||||
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
||||||
}
|
}
|
||||||
|
function taskBriefNe(brief) {
|
||||||
|
const rec = asRecord(brief);
|
||||||
|
const name = typeof rec.ne_name === "string" ? rec.ne_name.trim() : "";
|
||||||
|
const ip = typeof rec.ne_ip === "string" ? rec.ne_ip.trim() : "";
|
||||||
|
if (name && ip)
|
||||||
|
return `${name} (${ip})`;
|
||||||
|
return name || ip;
|
||||||
|
}
|
||||||
|
function slimMigrationProject(row) {
|
||||||
|
const mt = asRecord(row.monitor_template);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
note: row.note,
|
||||||
|
monitor_template_id: row.monitor_template_id,
|
||||||
|
monitor_template_name: typeof mt.name === "string" ? mt.name : "",
|
||||||
|
old_task_id: row.old_task_id,
|
||||||
|
new_task_id: row.new_task_id,
|
||||||
|
old_hf_task_id: row.old_hf_task_id,
|
||||||
|
new_hf_task_id: row.new_hf_task_id,
|
||||||
|
old_ne: taskBriefNe(row.old_task),
|
||||||
|
new_ne: taskBriefNe(row.new_task),
|
||||||
|
hf_interval_sec: row.hf_interval_sec,
|
||||||
|
hf_start_at: row.hf_start_at,
|
||||||
|
hf_end_at: row.hf_end_at,
|
||||||
|
updated_at: row.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function matchesBizMonitorQ(row, q) {
|
||||||
|
if (!q)
|
||||||
|
return true;
|
||||||
|
const hay = [
|
||||||
|
row.id,
|
||||||
|
row.name,
|
||||||
|
row.note,
|
||||||
|
row.status,
|
||||||
|
row.ne_name,
|
||||||
|
row.ne_ip,
|
||||||
|
row.ne_id,
|
||||||
|
row.old_ne,
|
||||||
|
row.new_ne,
|
||||||
|
row.monitor_template_name,
|
||||||
|
row.purpose,
|
||||||
|
row.vendor
|
||||||
|
].map((v) => typeof v === "string" || typeof v === "number" ? String(v).toLowerCase() : "").join(" ");
|
||||||
|
return hay.includes(q);
|
||||||
|
}
|
||||||
|
async function listBizMonitors(client, args, signal) {
|
||||||
|
const kindRaw = str2(args, "kind", "all").trim().toLowerCase() || "all";
|
||||||
|
const kind = kindRaw === "project" ? "projects" : kindRaw === "task" ? "tasks" : kindRaw;
|
||||||
|
if (kind !== "all" && kind !== "projects" && kind !== "tasks") {
|
||||||
|
return { ok: false, error: "kind_invalid", detail: "kind must be all|projects|tasks" };
|
||||||
|
}
|
||||||
|
const purpose = str2(args, "purpose").trim();
|
||||||
|
const status = str2(args, "status").trim().toLowerCase();
|
||||||
|
const q = str2(args, "q").trim().toLowerCase();
|
||||||
|
const limit = clampInt(num2(args, "limit") ?? 100, 100, 1, 500);
|
||||||
|
const wantProjects = kind === "all" || kind === "projects";
|
||||||
|
const wantTasks = kind === "all" || kind === "tasks";
|
||||||
|
const fetches = [];
|
||||||
|
if (wantProjects)
|
||||||
|
fetches.push(client.get("/v1/biz-migration/projects", {}, signal));
|
||||||
|
else
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
if (wantTasks) {
|
||||||
|
const params = {};
|
||||||
|
if (purpose)
|
||||||
|
params.purpose = purpose;
|
||||||
|
fetches.push(client.get("/v1/biz-state/tasks", params, signal));
|
||||||
|
} else {
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
}
|
||||||
|
const [projRes, taskRes] = await Promise.all(fetches);
|
||||||
|
if (wantProjects && projRes.ok === false)
|
||||||
|
return projRes;
|
||||||
|
if (wantTasks && taskRes.ok === false)
|
||||||
|
return taskRes;
|
||||||
|
const projItems = Array.isArray(asRecord(projRes.data).items) ? asRecord(projRes.data).items : [];
|
||||||
|
const taskItems = Array.isArray(asRecord(taskRes.data).items) ? asRecord(taskRes.data).items : [];
|
||||||
|
let projects = projItems.map((row) => slimMigrationProject(asRecord(row))).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
let tasks = taskItems.map((row) => asRecord(row)).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
const projectsTotal = projects.length;
|
||||||
|
const tasksTotal = tasks.length;
|
||||||
|
projects = projects.slice(0, limit);
|
||||||
|
tasks = tasks.slice(0, limit);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
kind,
|
||||||
|
projects: wantProjects ? projects : undefined,
|
||||||
|
tasks: wantTasks ? tasks : undefined,
|
||||||
|
counts: {
|
||||||
|
projects: wantProjects ? projectsTotal : 0,
|
||||||
|
tasks: wantTasks ? tasksTotal : 0,
|
||||||
|
projects_returned: wantProjects ? projects.length : 0,
|
||||||
|
tasks_returned: wantTasks ? tasks.length : 0
|
||||||
|
},
|
||||||
|
next: "Pass project_id (or task_id) to netx__getBizMonitorContext for the full definition bundle."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
async function getBizMonitorContext(client, args, signal) {
|
async function getBizMonitorContext(client, args, signal) {
|
||||||
const projectId = str2(args, "project_id").trim();
|
const projectId = str2(args, "project_id").trim();
|
||||||
const taskId = str2(args, "task_id").trim();
|
const taskId = str2(args, "task_id").trim();
|
||||||
|
|
@ -2355,7 +2466,14 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
layer: str4(),
|
layer: str4(),
|
||||||
detail: str4("summary | full")
|
detail: str4("summary | full")
|
||||||
}, findTopologyPaths, getClient, t),
|
}, findTopologyPaths, getClient, t),
|
||||||
tool2("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.", {
|
tool2("netx__listBizMonitors", "Catalog: list cutover/monitor-compare projects and biz_state monitor tasks. No project_id needed — call this first when the user did not give an id, then use getBizMonitorContext.", {
|
||||||
|
kind: str4("all | projects | tasks (default all)"),
|
||||||
|
purpose: str4("Filter tasks: portrait | cutover_hf | empty=all"),
|
||||||
|
status: str4("Filter by status (e.g. active / paused)"),
|
||||||
|
q: str4("Keyword on name / ne_name / note / id"),
|
||||||
|
limit: num4("Max rows per list (default 100, max 500)")
|
||||||
|
}, listBizMonitors, getClient, t),
|
||||||
|
tool2("netx__getBizMonitorContext", "Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id from listBizMonitors. Display keys are device-raw A/B.", {
|
||||||
project_id: str4("Cutover / biz-migration project id"),
|
project_id: str4("Cutover / biz-migration project id"),
|
||||||
task_id: str4("biz_state task id (optional if project_id set)")
|
task_id: str4("biz_state task id (optional if project_id set)")
|
||||||
}, getBizMonitorContext, getClient, t),
|
}, getBizMonitorContext, getClient, t),
|
||||||
|
|
|
||||||
120
lib/index.js
120
lib/index.js
|
|
@ -611,6 +611,7 @@ var TOOLS_BY_GROUP = Object.freeze({
|
||||||
"netx__sinkTopologyDualUnits"
|
"netx__sinkTopologyDualUnits"
|
||||||
]),
|
]),
|
||||||
bizMonitor: Object.freeze([
|
bizMonitor: Object.freeze([
|
||||||
|
"netx__listBizMonitors",
|
||||||
"netx__getBizMonitorContext",
|
"netx__getBizMonitorContext",
|
||||||
"netx__getBizMonitorBoard",
|
"netx__getBizMonitorBoard",
|
||||||
"netx__listBizMonitorReds",
|
"netx__listBizMonitorReds",
|
||||||
|
|
@ -3728,6 +3729,116 @@ async function findTopologyPaths(client, args, signal) {
|
||||||
body.to_managed_ne_id = toMid;
|
body.to_managed_ne_id = toMid;
|
||||||
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
return client.post("/v1/topology/fabric/paths", body, signal, 30000);
|
||||||
}
|
}
|
||||||
|
function taskBriefNe(brief) {
|
||||||
|
const rec = asRecord(brief);
|
||||||
|
const name = typeof rec.ne_name === "string" ? rec.ne_name.trim() : "";
|
||||||
|
const ip = typeof rec.ne_ip === "string" ? rec.ne_ip.trim() : "";
|
||||||
|
if (name && ip)
|
||||||
|
return `${name} (${ip})`;
|
||||||
|
return name || ip;
|
||||||
|
}
|
||||||
|
function slimMigrationProject(row) {
|
||||||
|
const mt = asRecord(row.monitor_template);
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
note: row.note,
|
||||||
|
monitor_template_id: row.monitor_template_id,
|
||||||
|
monitor_template_name: typeof mt.name === "string" ? mt.name : "",
|
||||||
|
old_task_id: row.old_task_id,
|
||||||
|
new_task_id: row.new_task_id,
|
||||||
|
old_hf_task_id: row.old_hf_task_id,
|
||||||
|
new_hf_task_id: row.new_hf_task_id,
|
||||||
|
old_ne: taskBriefNe(row.old_task),
|
||||||
|
new_ne: taskBriefNe(row.new_task),
|
||||||
|
hf_interval_sec: row.hf_interval_sec,
|
||||||
|
hf_start_at: row.hf_start_at,
|
||||||
|
hf_end_at: row.hf_end_at,
|
||||||
|
updated_at: row.updated_at
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function matchesBizMonitorQ(row, q) {
|
||||||
|
if (!q)
|
||||||
|
return true;
|
||||||
|
const hay = [
|
||||||
|
row.id,
|
||||||
|
row.name,
|
||||||
|
row.note,
|
||||||
|
row.status,
|
||||||
|
row.ne_name,
|
||||||
|
row.ne_ip,
|
||||||
|
row.ne_id,
|
||||||
|
row.old_ne,
|
||||||
|
row.new_ne,
|
||||||
|
row.monitor_template_name,
|
||||||
|
row.purpose,
|
||||||
|
row.vendor
|
||||||
|
].map((v) => typeof v === "string" || typeof v === "number" ? String(v).toLowerCase() : "").join(" ");
|
||||||
|
return hay.includes(q);
|
||||||
|
}
|
||||||
|
async function listBizMonitors(client, args, signal) {
|
||||||
|
const kindRaw = str2(args, "kind", "all").trim().toLowerCase() || "all";
|
||||||
|
const kind = kindRaw === "project" ? "projects" : kindRaw === "task" ? "tasks" : kindRaw;
|
||||||
|
if (kind !== "all" && kind !== "projects" && kind !== "tasks") {
|
||||||
|
return { ok: false, error: "kind_invalid", detail: "kind must be all|projects|tasks" };
|
||||||
|
}
|
||||||
|
const purpose = str2(args, "purpose").trim();
|
||||||
|
const status = str2(args, "status").trim().toLowerCase();
|
||||||
|
const q = str2(args, "q").trim().toLowerCase();
|
||||||
|
const limit = clampInt(num2(args, "limit") ?? 100, 100, 1, 500);
|
||||||
|
const wantProjects = kind === "all" || kind === "projects";
|
||||||
|
const wantTasks = kind === "all" || kind === "tasks";
|
||||||
|
const fetches = [];
|
||||||
|
if (wantProjects)
|
||||||
|
fetches.push(client.get("/v1/biz-migration/projects", {}, signal));
|
||||||
|
else
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
if (wantTasks) {
|
||||||
|
const params = {};
|
||||||
|
if (purpose)
|
||||||
|
params.purpose = purpose;
|
||||||
|
fetches.push(client.get("/v1/biz-state/tasks", params, signal));
|
||||||
|
} else {
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }));
|
||||||
|
}
|
||||||
|
const [projRes, taskRes] = await Promise.all(fetches);
|
||||||
|
if (wantProjects && projRes.ok === false)
|
||||||
|
return projRes;
|
||||||
|
if (wantTasks && taskRes.ok === false)
|
||||||
|
return taskRes;
|
||||||
|
const projItems = Array.isArray(asRecord(projRes.data).items) ? asRecord(projRes.data).items : [];
|
||||||
|
const taskItems = Array.isArray(asRecord(taskRes.data).items) ? asRecord(taskRes.data).items : [];
|
||||||
|
let projects = projItems.map((row) => slimMigrationProject(asRecord(row))).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
let tasks = taskItems.map((row) => asRecord(row)).filter((row) => {
|
||||||
|
if (status && String(row.status || "").toLowerCase() !== status)
|
||||||
|
return false;
|
||||||
|
return matchesBizMonitorQ(row, q);
|
||||||
|
});
|
||||||
|
const projectsTotal = projects.length;
|
||||||
|
const tasksTotal = tasks.length;
|
||||||
|
projects = projects.slice(0, limit);
|
||||||
|
tasks = tasks.slice(0, limit);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
kind,
|
||||||
|
projects: wantProjects ? projects : undefined,
|
||||||
|
tasks: wantTasks ? tasks : undefined,
|
||||||
|
counts: {
|
||||||
|
projects: wantProjects ? projectsTotal : 0,
|
||||||
|
tasks: wantTasks ? tasksTotal : 0,
|
||||||
|
projects_returned: wantProjects ? projects.length : 0,
|
||||||
|
tasks_returned: wantTasks ? tasks.length : 0
|
||||||
|
},
|
||||||
|
next: "Pass project_id (or task_id) to netx__getBizMonitorContext for the full definition bundle."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
async function getBizMonitorContext(client, args, signal) {
|
async function getBizMonitorContext(client, args, signal) {
|
||||||
const projectId = str2(args, "project_id").trim();
|
const projectId = str2(args, "project_id").trim();
|
||||||
const taskId = str2(args, "task_id").trim();
|
const taskId = str2(args, "task_id").trim();
|
||||||
|
|
@ -4614,7 +4725,14 @@ function registerNetxTools(ctx, connection, options) {
|
||||||
layer: str4(),
|
layer: str4(),
|
||||||
detail: str4("summary | full")
|
detail: str4("summary | full")
|
||||||
}, findTopologyPaths, getClient, t),
|
}, findTopologyPaths, getClient, t),
|
||||||
tool2("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.", {
|
tool2("netx__listBizMonitors", "Catalog: list cutover/monitor-compare projects and biz_state monitor tasks. No project_id needed — call this first when the user did not give an id, then use getBizMonitorContext.", {
|
||||||
|
kind: str4("all | projects | tasks (default all)"),
|
||||||
|
purpose: str4("Filter tasks: portrait | cutover_hf | empty=all"),
|
||||||
|
status: str4("Filter by status (e.g. active / paused)"),
|
||||||
|
q: str4("Keyword on name / ne_name / note / id"),
|
||||||
|
limit: num4("Max rows per list (default 100, max 500)")
|
||||||
|
}, listBizMonitors, getClient, t),
|
||||||
|
tool2("netx__getBizMonitorContext", "Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id from listBizMonitors. Display keys are device-raw A/B.", {
|
||||||
project_id: str4("Cutover / biz-migration project id"),
|
project_id: str4("Cutover / biz-migration project id"),
|
||||||
task_id: str4("biz_state task id (optional if project_id set)")
|
task_id: str4("biz_state task id (optional if project_id set)")
|
||||||
}, getBizMonitorContext, getClient, t),
|
}, getBizMonitorContext, getClient, t),
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,22 @@
|
||||||
name: netx-biz-monitor
|
name: netx-biz-monitor
|
||||||
description: >-
|
description: >-
|
||||||
Overnight cutover / biz_state monitor analysis via netxops host tools
|
Overnight cutover / biz_state monitor analysis via netxops host tools
|
||||||
(netx__getBizMonitor*). Given a project_id or task_id, read definitions,
|
(netx__listBizMonitors / netx__getBizMonitor*). List projects/tasks first
|
||||||
board, reds/diffs with evidence (device-raw A/B, show commands), and judge
|
when no id is given; then read definitions, board, reds/diffs with evidence
|
||||||
tool false-positive vs real business fault. Does not create templates/tasks.
|
(device-raw A/B, show commands), and judge tool false-positive vs real
|
||||||
|
business fault. Does not create templates/tasks.
|
||||||
---
|
---
|
||||||
|
|
||||||
# netx-biz-monitor(割接 / 业务监控只读分析)
|
# netx-biz-monitor(割接 / 业务监控只读分析)
|
||||||
|
|
||||||
人配好监控与任务;AI 当晚用 **project_id**(或 task_id)做归因。
|
人配好监控与任务;AI 当晚做归因。
|
||||||
工具在 **netxops**(`netx__*`),直连 netx REST,**不是** netx-mcp。
|
工具在 **netxops**(`netx__*`),直连 netx REST,**不是** netx-mcp。
|
||||||
|
|
||||||
## 工具(能力组 `bizMonitor`)
|
## 工具(能力组 `bizMonitor`)
|
||||||
|
|
||||||
| 工具 | 用途 |
|
| 工具 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
| `netx__listBizMonitors` | **入口目录**:列出割接/监控对比项目 + 业务监控任务(**无需** project_id) |
|
||||||
| `netx__getBizMonitorContext` | 任务定义:项目、监控/对比模板、归一化规则、端口映射、关联任务与命令项 |
|
| `netx__getBizMonitorContext` | 任务定义:项目、监控/对比模板、归一化规则、端口映射、关联任务与命令项 |
|
||||||
| `netx__getBizMonitorBoard` | 批次进度、evaluate run、sheet 卡、verdict 统计 |
|
| `netx__getBizMonitorBoard` | 批次进度、evaluate run、sheet 卡、verdict 统计 |
|
||||||
| `netx__listBizMonitorReds` | 红单 + **evidence** |
|
| `netx__listBizMonitorReds` | 红单 + **evidence** |
|
||||||
|
|
@ -25,7 +27,7 @@ description: >-
|
||||||
|
|
||||||
交叉验证真障时再用 ops:`netx__execManagedNe` / 拓扑路径。
|
交叉验证真障时再用 ops:`netx__execManagedNe` / 拓扑路径。
|
||||||
|
|
||||||
## 关键约定(必守)
|
## 字段约定(必守)
|
||||||
|
|
||||||
- **`old_key` / `new_key`**:设备采集原文 **A/B**(大屏与结论用这个)
|
- **`old_key` / `new_key`**:设备采集原文 **A/B**(大屏与结论用这个)
|
||||||
- **`match_*` / evidence.iface.normalized|map_***:内部配对,**不要**把映射后的 BB 说成两侧端口
|
- **`match_*` / evidence.iface.normalized|map_***:内部配对,**不要**把映射后的 BB 说成两侧端口
|
||||||
|
|
@ -34,12 +36,13 @@ description: >-
|
||||||
|
|
||||||
## 推荐顺序
|
## 推荐顺序
|
||||||
|
|
||||||
1. `getBizMonitorContext(project_id)` — 弄清盯什么
|
1. 无 id → `listBizMonitors`(`kind=projects|tasks|all`,可加 `q` / `purpose`)
|
||||||
2. `getBizMonitorBoard(batch_id)` — 进度 / missing / skipped
|
2. `getBizMonitorContext(project_id)` — 弄清盯什么
|
||||||
3. `listBizMonitorReds` 或 `getBizMonitorDiffs(color=red)`
|
3. `getBizMonitorBoard(batch_id)` — 进度 / missing / skipped
|
||||||
4. 看 evidence:parse 失败?current_missing?映射 miss?归一化误伤?
|
4. `listBizMonitorReds` 或 `getBizMonitorDiffs(color=red)`
|
||||||
5. 需要原文 → `getBizCollectCommandRaw`
|
5. 看 evidence:parse 失败?current_missing?映射 miss?归一化误伤?
|
||||||
6. 像真障 → `execManagedNe` 现场核对
|
6. 需要原文 → `getBizCollectCommandRaw`
|
||||||
|
7. 像真障 → `execManagedNe` 现场核对
|
||||||
|
|
||||||
## Verdict 速查(skill 解释,工具不硬编码)
|
## Verdict 速查(skill 解释,工具不硬编码)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ export const TOOLS_BY_GROUP: Readonly<Record<NetxCapabilityGroupId, readonly str
|
||||||
'netx__sinkTopologyDualUnits',
|
'netx__sinkTopologyDualUnits',
|
||||||
]),
|
]),
|
||||||
bizMonitor: Object.freeze([
|
bizMonitor: Object.freeze([
|
||||||
|
'netx__listBizMonitors',
|
||||||
'netx__getBizMonitorContext',
|
'netx__getBizMonitorContext',
|
||||||
'netx__getBizMonitorBoard',
|
'netx__getBizMonitorBoard',
|
||||||
'netx__listBizMonitorReds',
|
'netx__listBizMonitorReds',
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,128 @@ export async function findTopologyPaths(client: NetxClient, args: NetxJson, sign
|
||||||
|
|
||||||
// ── bizMonitor group (cutover / biz_state read) ─────────────────────────────
|
// ── bizMonitor group (cutover / biz_state read) ─────────────────────────────
|
||||||
|
|
||||||
|
function taskBriefNe(brief: unknown): string {
|
||||||
|
const rec = asRecord(brief)
|
||||||
|
const name = typeof rec.ne_name === 'string' ? rec.ne_name.trim() : ''
|
||||||
|
const ip = typeof rec.ne_ip === 'string' ? rec.ne_ip.trim() : ''
|
||||||
|
if (name && ip) return `${name} (${ip})`
|
||||||
|
return name || ip
|
||||||
|
}
|
||||||
|
|
||||||
|
function slimMigrationProject(row: NetxJson): NetxJson {
|
||||||
|
const mt = asRecord(row.monitor_template)
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
status: row.status,
|
||||||
|
note: row.note,
|
||||||
|
monitor_template_id: row.monitor_template_id,
|
||||||
|
monitor_template_name: typeof mt.name === 'string' ? mt.name : '',
|
||||||
|
old_task_id: row.old_task_id,
|
||||||
|
new_task_id: row.new_task_id,
|
||||||
|
old_hf_task_id: row.old_hf_task_id,
|
||||||
|
new_hf_task_id: row.new_hf_task_id,
|
||||||
|
old_ne: taskBriefNe(row.old_task),
|
||||||
|
new_ne: taskBriefNe(row.new_task),
|
||||||
|
hf_interval_sec: row.hf_interval_sec,
|
||||||
|
hf_start_at: row.hf_start_at,
|
||||||
|
hf_end_at: row.hf_end_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesBizMonitorQ(row: NetxJson, q: string): boolean {
|
||||||
|
if (!q) return true
|
||||||
|
const hay = [
|
||||||
|
row.id, row.name, row.note, row.status, row.ne_name, row.ne_ip, row.ne_id,
|
||||||
|
row.old_ne, row.new_ne, row.monitor_template_name, row.purpose, row.vendor,
|
||||||
|
]
|
||||||
|
.map((v) => (typeof v === 'string' || typeof v === 'number' ? String(v).toLowerCase() : ''))
|
||||||
|
.join(' ')
|
||||||
|
return hay.includes(q)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catalog entry: list cutover projects and/or biz_state tasks (no project_id required).
|
||||||
|
* Use this first when the user did not give an id; then call getBizMonitorContext.
|
||||||
|
*/
|
||||||
|
export async function listBizMonitors(
|
||||||
|
client: NetxClient,
|
||||||
|
args: NetxJson,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<NetxJson> {
|
||||||
|
const kindRaw = str(args, 'kind', 'all').trim().toLowerCase() || 'all'
|
||||||
|
const kind = kindRaw === 'project' ? 'projects'
|
||||||
|
: kindRaw === 'task' ? 'tasks'
|
||||||
|
: kindRaw
|
||||||
|
if (kind !== 'all' && kind !== 'projects' && kind !== 'tasks') {
|
||||||
|
return { ok: false, error: 'kind_invalid', detail: 'kind must be all|projects|tasks' }
|
||||||
|
}
|
||||||
|
const purpose = str(args, 'purpose').trim()
|
||||||
|
const status = str(args, 'status').trim().toLowerCase()
|
||||||
|
const q = str(args, 'q').trim().toLowerCase()
|
||||||
|
const limit = clampInt(num(args, 'limit') ?? 100, 100, 1, 500)
|
||||||
|
|
||||||
|
const wantProjects = kind === 'all' || kind === 'projects'
|
||||||
|
const wantTasks = kind === 'all' || kind === 'tasks'
|
||||||
|
|
||||||
|
const fetches: Array<Promise<NetxJson>> = []
|
||||||
|
if (wantProjects) fetches.push(client.get('/v1/biz-migration/projects', {}, signal))
|
||||||
|
else fetches.push(Promise.resolve({ ok: true, data: { items: [] } }))
|
||||||
|
if (wantTasks) {
|
||||||
|
const params: Record<string, string | number | boolean> = {}
|
||||||
|
if (purpose) params.purpose = purpose
|
||||||
|
fetches.push(client.get('/v1/biz-state/tasks', params, signal))
|
||||||
|
} else {
|
||||||
|
fetches.push(Promise.resolve({ ok: true, data: { items: [] } }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const [projRes, taskRes] = await Promise.all(fetches)
|
||||||
|
if (wantProjects && projRes.ok === false) return projRes
|
||||||
|
if (wantTasks && taskRes.ok === false) return taskRes
|
||||||
|
|
||||||
|
const projItems = Array.isArray(asRecord(projRes.data).items)
|
||||||
|
? (asRecord(projRes.data).items as unknown[])
|
||||||
|
: []
|
||||||
|
const taskItems = Array.isArray(asRecord(taskRes.data).items)
|
||||||
|
? (asRecord(taskRes.data).items as unknown[])
|
||||||
|
: []
|
||||||
|
|
||||||
|
let projects = projItems
|
||||||
|
.map((row) => slimMigrationProject(asRecord(row)))
|
||||||
|
.filter((row) => {
|
||||||
|
if (status && String(row.status || '').toLowerCase() !== status) return false
|
||||||
|
return matchesBizMonitorQ(row, q)
|
||||||
|
})
|
||||||
|
let tasks = taskItems
|
||||||
|
.map((row) => asRecord(row))
|
||||||
|
.filter((row) => {
|
||||||
|
if (status && String(row.status || '').toLowerCase() !== status) return false
|
||||||
|
return matchesBizMonitorQ(row, q)
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectsTotal = projects.length
|
||||||
|
const tasksTotal = tasks.length
|
||||||
|
projects = projects.slice(0, limit)
|
||||||
|
tasks = tasks.slice(0, limit)
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
kind,
|
||||||
|
projects: wantProjects ? projects : undefined,
|
||||||
|
tasks: wantTasks ? tasks : undefined,
|
||||||
|
counts: {
|
||||||
|
projects: wantProjects ? projectsTotal : 0,
|
||||||
|
tasks: wantTasks ? tasksTotal : 0,
|
||||||
|
projects_returned: wantProjects ? projects.length : 0,
|
||||||
|
tasks_returned: wantTasks ? tasks.length : 0,
|
||||||
|
},
|
||||||
|
next: 'Pass project_id (or task_id) to netx__getBizMonitorContext for the full definition bundle.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Fat cutover / biz_state definition bundle (templates, mapping, tasks). */
|
/** Fat cutover / biz_state definition bundle (templates, mapping, tasks). */
|
||||||
export async function getBizMonitorContext(
|
export async function getBizMonitorContext(
|
||||||
client: NetxClient,
|
client: NetxClient,
|
||||||
|
|
|
||||||
|
|
@ -317,9 +317,21 @@ export function registerNetxTools(
|
||||||
H.findTopologyPaths, getClient, t,
|
H.findTopologyPaths, getClient, t,
|
||||||
),
|
),
|
||||||
// ── bizMonitor group (cutover / biz_state read via netx REST) ─────────
|
// ── bizMonitor group (cutover / biz_state read via netx REST) ─────────
|
||||||
|
tool(
|
||||||
|
'netx__listBizMonitors',
|
||||||
|
'Catalog: list cutover/monitor-compare projects and biz_state monitor tasks. No project_id needed — call this first when the user did not give an id, then use getBizMonitorContext.',
|
||||||
|
{
|
||||||
|
kind: str('all | projects | tasks (default all)'),
|
||||||
|
purpose: str('Filter tasks: portrait | cutover_hf | empty=all'),
|
||||||
|
status: str('Filter by status (e.g. active / paused)'),
|
||||||
|
q: str('Keyword on name / ne_name / note / id'),
|
||||||
|
limit: num('Max rows per list (default 100, max 500)'),
|
||||||
|
},
|
||||||
|
H.listBizMonitors, getClient, t,
|
||||||
|
),
|
||||||
tool(
|
tool(
|
||||||
'netx__getBizMonitorContext',
|
'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.',
|
'Biz-monitor definition bundle: cutover project + monitor/compare templates + iface normalize + port mapping + related tasks/commands. Prefer project_id from listBizMonitors. Display keys are device-raw A/B.',
|
||||||
{
|
{
|
||||||
project_id: str('Cutover / biz-migration project id'),
|
project_id: str('Cutover / biz-migration project id'),
|
||||||
task_id: str('biz_state task id (optional if project_id set)'),
|
task_id: str('biz_state task id (optional if project_id set)'),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue