Add listBizMonitorBatches so agents can discover cutover/collect batch and run ids.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-20 15:26:46 +08:00
parent c1d07f7509
commit b235957c34
12 changed files with 1518 additions and 26 deletions

View file

@ -41,6 +41,7 @@ var TOOLS_BY_GROUP = Object.freeze({
]),
bizMonitor: Object.freeze([
"netx__listBizMonitors",
"netx__listBizMonitorBatches",
"netx__getBizMonitorContext",
"netx__getBizMonitorBoard",
"netx__listBizMonitorReds",
@ -1576,7 +1577,176 @@ async function listBizMonitors(client, args, signal) {
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."
next: "Pass project_id (or task_id) to netx__getBizMonitorContext; then listBizMonitorBatches for batch_id / run_id."
}
};
}
function slimCutoverBatch(row) {
return {
id: row.id,
project_id: row.project_id,
batch_label: row.batch_label,
status: row.status,
accept_status: row.accept_status,
accept_run_id: row.accept_run_id,
started_at: row.started_at,
ended_at: row.ended_at,
note: row.note,
created_at: row.created_at,
updated_at: row.updated_at
};
}
function slimCollectBatch(row, taskId = "") {
return {
id: row.id,
task_id: row.task_id || taskId,
status: row.status,
ne_name: row.ne_name,
command_count: row.command_count,
row_count: row.row_count,
is_baseline: row.is_baseline,
protected: row.protected,
message: row.message,
started_at: row.started_at,
ended_at: row.ended_at
};
}
function slimEvalRun(row) {
const summary = asRecord(row.summary);
const progress = asRecord(summary.progress);
return {
id: row.id,
batch_id: row.batch_id,
purpose: row.purpose,
old_batch_id: row.old_batch_id,
new_batch_id: row.new_batch_id,
created_at: row.created_at,
anomaly: summary.anomaly,
progress_ok: progress.ok ?? summary.progress_ok,
progress_total: progress.total ?? summary.progress_total
};
}
async function listBizMonitorBatches(client, args, signal) {
const projectId = str2(args, "project_id").trim();
const taskId = str2(args, "task_id").trim();
const batchId = str2(args, "batch_id").trim();
const kindRaw = str2(args, "kind", "auto").trim().toLowerCase() || "auto";
const includeRuns = args.include_runs === true || args.include_runs === "true" || args.include_runs === 1;
const status = str2(args, "status").trim().toLowerCase();
const limit = clampInt(num2(args, "limit") ?? 50, 50, 1, 200);
const runsLimit = clampInt(num2(args, "runs_limit") ?? 10, 10, 1, 50);
let kind = kindRaw;
if (kind === "auto") {
if (batchId && !projectId && !taskId)
kind = "runs";
else if (projectId && !taskId)
kind = "cutover";
else if (taskId && !projectId)
kind = "collect";
else if (projectId && taskId)
kind = "all";
else if (batchId)
kind = "runs";
else {
return {
ok: false,
error: "project_id_or_task_id_or_batch_id_required",
detail: "Pass project_id (cutover batches), task_id (collect batches), and/or batch_id (evaluate runs)."
};
}
}
if (!["auto", "all", "cutover", "collect", "runs"].includes(kind)) {
return { ok: false, error: "kind_invalid", detail: "kind must be auto|all|cutover|collect|runs" };
}
const wantCutover = kind === "all" || kind === "cutover";
const wantCollect = kind === "all" || kind === "collect";
const wantRuns = kind === "runs" || includeRuns && (wantCutover || Boolean(batchId));
if (wantCutover && !projectId) {
return { ok: false, error: "project_id_required", detail: "kind=cutover|all needs project_id" };
}
if (wantCollect && !taskId) {
return { ok: false, error: "task_id_required", detail: "kind=collect|all needs task_id" };
}
if (kind === "runs" && !batchId) {
return { ok: false, error: "batch_id_required", detail: "kind=runs needs cutover batch_id" };
}
let cutoverBatches = [];
let collectBatches = [];
let runs = [];
if (wantCutover) {
const res = await client.get(`/v1/biz-migration/projects/${encodeURIComponent(projectId)}/batches`, {}, signal);
if (res.ok === false)
return res;
const items = Array.isArray(asRecord(res.data).items) ? asRecord(res.data).items : [];
cutoverBatches = items.map((row) => slimCutoverBatch(asRecord(row))).filter((row) => {
if (batchId && String(row.id || "") !== batchId)
return false;
if (status && String(row.status || "").toLowerCase() !== status)
return false;
return true;
}).slice(0, limit);
}
if (wantCollect) {
const res = await client.get(`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/batches`, { limit }, signal);
if (res.ok === false)
return res;
const items = Array.isArray(asRecord(res.data).items) ? asRecord(res.data).items : [];
collectBatches = items.map((row) => slimCollectBatch(asRecord(row), taskId)).filter((row) => {
if (status && String(row.status || "").toLowerCase() !== status)
return false;
return true;
}).slice(0, limit);
}
if (wantRuns) {
const runBatchIds = kind === "runs" ? [batchId] : cutoverBatches.map((b) => String(b.id || "").trim()).filter(Boolean).slice(0, includeRuns ? 5 : 0);
if (kind === "runs" && batchId) {
const res = await client.get(`/v1/biz-migration/batches/${encodeURIComponent(batchId)}/runs`, { limit: runsLimit }, signal);
if (res.ok === false)
return res;
const items = Array.isArray(asRecord(res.data).items) ? asRecord(res.data).items : [];
runs = items.map((row) => slimEvalRun(asRecord(row))).slice(0, runsLimit);
} else if (includeRuns && runBatchIds.length > 0) {
const settled = await Promise.all(runBatchIds.map(async (id) => {
const res = await client.get(`/v1/biz-migration/batches/${encodeURIComponent(id)}/runs`, { limit: runsLimit }, signal);
if (res.ok === false)
return { batch_id: id, error: res, items: [] };
const items = Array.isArray(asRecord(res.data).items) ? asRecord(res.data).items : [];
return {
batch_id: id,
items: items.map((row) => slimEvalRun(asRecord(row))).slice(0, runsLimit)
};
}));
for (const block of settled) {
if ("error" in block && block.error) {
const failed = cutoverBatches.find((b) => b.id === block.batch_id);
if (failed)
failed.runs_error = asRecord(block.error).error || "runs_fetch_failed";
continue;
}
const target = cutoverBatches.find((b) => b.id === block.batch_id);
if (target)
target.runs = block.items;
for (const r of block.items)
runs.push(r);
}
}
}
return {
ok: true,
data: {
kind,
project_id: projectId || undefined,
task_id: taskId || undefined,
batch_id: batchId || undefined,
cutover_batches: wantCutover ? cutoverBatches : undefined,
collect_batches: wantCollect ? collectBatches : undefined,
runs: kind === "runs" ? runs : undefined,
counts: {
cutover_batches: cutoverBatches.length,
collect_batches: collectBatches.length,
runs: runs.length
},
next: wantCutover ? "Use cutover batch id with netx__getBizMonitorBoard / listBizMonitorBatches(kind=runs). Collect batch id → netx__getBizCollectBatch." : wantCollect ? "Pass collect batch id to netx__getBizCollectBatch / getBizCollectCommandRaw." : "Pass run_id to netx__getBizMonitorDiffs or getBizMonitorBoard(run_id)."
}
};
}
@ -2466,13 +2636,23 @@ function registerNetxTools(ctx, connection, options) {
layer: str4(),
detail: str4("summary | full")
}, findTopologyPaths, getClient, t),
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.", {
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 listBizMonitorBatches / 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__listBizMonitorBatches", "List batch ids for further analysis. project_id → cutover batches; task_id → biz_state collect batches; batch_id + kind=runs → evaluate runs. Optional include_runs attaches recent runs onto cutover batches.", {
kind: str4("auto | all | cutover | collect | runs (default auto)"),
project_id: str4("Cutover project id → cutover_batches"),
task_id: str4("biz_state task id → collect_batches"),
batch_id: str4("Cutover batch id (filter or kind=runs)"),
include_runs: bool4("Attach recent evaluate runs onto listed cutover batches"),
status: str4("Filter batch status"),
limit: num4("Max batches (default 50)"),
runs_limit: num4("Max runs per batch when include_runs / kind=runs (default 10)")
}, listBizMonitorBatches, 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"),
task_id: str4("biz_state task id (optional if project_id set)")