Add one-click export of biz-state collect commands.

Plan enabled monitoring items into concrete CLIs (with aux) and download as txt from the task detail modal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-22 11:18:58 +08:00
parent 1fcd613bc6
commit 32deca1687
7 changed files with 446 additions and 1 deletions

View file

@ -258,6 +258,8 @@ const en = {
batches: "Batches",
viewBatch: "Open",
export: "Export",
exportCommands: "Export collect commands",
exportCommandsOk: "Collect commands exported",
noBatches: "No batches yet",
batchDetail: "Batch detail",
batchWorkbook: "Batch workbook",

View file

@ -258,6 +258,8 @@ const zh = {
batches: "采集批次",
viewBatch: "查看",
export: "导出",
exportCommands: "导出采集命令",
exportCommandsOk: "已导出采集命令",
noBatches: "尚无批次",
batchDetail: "批次详情",
batchWorkbook: "批次工作簿",

View file

@ -14,6 +14,7 @@ import {
bizStateDeleteTask,
bizStateDiscover,
bizStateDownloadExport,
bizStateDownloadTaskCommands,
bizStateGetBatch,
bizStateGetBatchCommand,
bizStateGetTask,
@ -747,6 +748,19 @@ export function BizStatePage() {
await collectNowForTask(taskId, true);
};
const exportTaskCommands = async () => {
if (!taskId) return;
setBusy(true);
try {
await bizStateDownloadTaskCommands(taskId);
showOk(t("bizState.exportCommandsOk"));
} catch (e) {
showError(formatErr(e));
} finally {
setBusy(false);
}
};
const removeTask = async (id: string) => {
if (!window.confirm(t("bizState.confirmDelete"))) return;
setBusy(true);
@ -1666,6 +1680,14 @@ export function BizStatePage() {
>
{t("bizState.collectNow")}
</Button>
<Button
size="sm"
variant="secondary"
isDisabled={busy || !taskId}
onPress={() => void exportTaskCommands()}
>
{t("bizState.exportCommands")}
</Button>
<Button size="sm" variant="danger" isDisabled={busy} onPress={() => void removeTask(taskId)}>
{t("bizState.delete")}
</Button>

View file

@ -1926,6 +1926,44 @@ export const bizStateDownloadExport = async (batchId: string): Promise<void> =>
}
};
export const bizStateDownloadTaskCommands = async (
taskId: string,
opts?: { includeAux?: boolean; enabledOnly?: boolean },
): Promise<void> => {
const q = new URLSearchParams();
if (opts?.includeAux === false) q.set("include_aux", "false");
if (opts?.enabledOnly === false) q.set("enabled_only", "false");
const qs = q.toString();
const path =
`/v1/biz-state/tasks/${encodeURIComponent(taskId)}/commands/export` +
(qs ? `?${qs}` : "");
const res = await fetch(path, { method: "GET", credentials: fetchCreds, headers: authHeaders() });
if (res.status === 401) {
handleUnauthorized(path);
throw new Error("unauthorized");
}
if (!res.ok) {
let detail = `${res.status} export commands`;
try {
const j = (await res.json()) as { detail?: unknown };
if (j?.detail != null) detail = typeof j.detail === "string" ? j.detail : JSON.stringify(j.detail);
} catch {
/* ignore */
}
throw new Error(detail);
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
try {
const a = document.createElement("a");
a.href = url;
a.download = `biz_state_commands_${taskId}.txt`;
a.click();
} finally {
URL.revokeObjectURL(url);
}
};
/* ---- biz_state compare (Phase2) ---- */
export const bizCompareListTemplates = () =>