feat(admin): Phase 6 AI任务监控+成本异常 — 全局任务列/筛/详情抽屉/失败重投
后端:adminpanel tasks(全局 AITask + status/type/team/anomaly 筛分页)+ detail(payload)+ retry(失败重投, 仅图像类 best-effort 否则 400);is_cost_anomaly(实际>预估×1.5)+ F() 异常筛;IsPlatformAdmin + 审计。 前端:adminApi tasks 系列;Admin 任务监控页(状态 tab + 仅成本异常 chip + 表格成本预估→实际+异常标 + 详情抽屉 payload + 失败重投)。 修真 bug:抽屉缺 .show 类停屏外,补上正常滑入。 测试:adminpanel 36 单测过(筛/异常/详情/重投 mock celery + 非失败/不支持/权限拒); 无头 e2e _admin-p6.mjs 6 断言过 + 0 console error(不点真重投);tsc+build 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5aab8a568f
commit
ca1e50d32c
@@ -0,0 +1,83 @@
|
||||
// Phase 6 e2e:AI 任务监控 —— 列表、失败/成本异常筛选、详情抽屉(payload)、重投按钮在场。
|
||||
// 不点真重投(避免触发真实生成);重投逻辑由后端 mock 单测覆盖。wrapper 预建一条 failed+异常 任务,跑完删。
|
||||
import { chromium } from "playwright";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const BASE = process.env.BASE || "http://127.0.0.1:5188";
|
||||
const API = process.env.API || "http://127.0.0.1:8010";
|
||||
const OUT = path.resolve("output/admin");
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
async function apiLogin(u, p) {
|
||||
const res = await fetch(`${API}/api/auth/login/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: u, password: p }) });
|
||||
return res.json();
|
||||
}
|
||||
const admin = await apiLogin("admin", "admin123");
|
||||
|
||||
const r = { page: {}, detail: {}, failedTab: {}, anomaly: {}, consoleErrors: [], pass: false };
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const hook = (p, tag) => {
|
||||
p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push(`${tag}:${m.text()}`); });
|
||||
p.on("pageerror", (e) => r.consoleErrors.push(`${tag}:PAGEERR:${e.message}`));
|
||||
};
|
||||
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
await ctx.addInitScript((tk) => localStorage.setItem("airshelf_token", tk), admin.token);
|
||||
const p = await ctx.newPage(); hook(p, "tasks");
|
||||
await p.goto(BASE + "/admin/tasks", { waitUntil: "load" });
|
||||
await p.waitForSelector(".admin-app", { timeout: 12000 });
|
||||
await p.waitForFunction(() => {
|
||||
const t = document.querySelector(".admin-table");
|
||||
const h3 = document.querySelector(".empty-state.show h3");
|
||||
return Boolean(t) || (h3 && h3.textContent !== "加载中…");
|
||||
}, { timeout: 20000 }).catch(() => {});
|
||||
r.page.title = (await p.locator(".content h1").first().innerText().catch(() => "")).trim();
|
||||
r.page.tabs = await p.locator(".tabs-sub .tab-sub").count();
|
||||
r.page.anomalyChip = (await p.locator(".admin-anomaly-chip").count()) >= 1;
|
||||
r.page.rows = await p.locator(".admin-table tbody tr").count();
|
||||
await p.screenshot({ path: path.join(OUT, "p6-tasks-list.png"), fullPage: true });
|
||||
|
||||
// 详情抽屉(第一行)
|
||||
if (r.page.rows >= 1) {
|
||||
await p.locator(".admin-table tbody tr:first-child .btn-ghost").first().click();
|
||||
await p.waitForSelector(".drawer", { timeout: 6000 });
|
||||
await p.waitForTimeout(500);
|
||||
r.detail.drawerShown = (await p.locator(".drawer").count()) >= 1;
|
||||
r.detail.hasJson = (await p.locator(".admin-json").count()) >= 1;
|
||||
await p.screenshot({ path: path.join(OUT, "p6-task-detail.png") });
|
||||
await p.locator(".drawer-bg").click({ position: { x: 12, y: 12 } }); // 点抽屉外遮罩关闭
|
||||
await p.waitForTimeout(400);
|
||||
}
|
||||
|
||||
// 失败筛选 → 该 tab 行(若有)首行带「重投」按钮
|
||||
await p.locator('.tabs-sub .tab-sub:has-text("失败")').click();
|
||||
await p.waitForTimeout(800);
|
||||
r.failedTab.rows = await p.locator(".admin-table tbody tr").count();
|
||||
if (r.failedTab.rows >= 1) {
|
||||
const btns = await p.locator(".admin-table tbody tr:first-child .col-actions .btn").allInnerTexts();
|
||||
r.failedTab.hasRetry = btns.some((t) => t.includes("重投"));
|
||||
}
|
||||
|
||||
// 成本异常筛选
|
||||
await p.locator(".admin-anomaly-chip").click();
|
||||
await p.waitForTimeout(800);
|
||||
r.anomaly.rows = await p.locator(".admin-table tbody tr").count();
|
||||
await p.screenshot({ path: path.join(OUT, "p6-anomaly.png"), fullPage: true });
|
||||
|
||||
await ctx.close();
|
||||
await browser.close();
|
||||
|
||||
const checks = {
|
||||
pageLoads: r.page.title === "任务监控" && r.page.tabs === 3 && r.page.anomalyChip === true,
|
||||
hasRows: r.page.rows >= 1,
|
||||
detailDrawer: r.detail.drawerShown === true && r.detail.hasJson === true,
|
||||
failedRetryBtn: r.failedTab.rows >= 1 && r.failedTab.hasRetry === true,
|
||||
anomalyFilter: r.anomaly.rows >= 1,
|
||||
zeroConsoleErrors: r.consoleErrors.length === 0,
|
||||
};
|
||||
r.checks = checks;
|
||||
r.pass = Object.values(checks).every(Boolean);
|
||||
console.log(JSON.stringify(r, null, 2));
|
||||
fs.writeFileSync(path.join(OUT, "p6-summary.json"), JSON.stringify(r, null, 2));
|
||||
process.exit(r.pass ? 0 : 1);
|
||||
Reference in New Issue
Block a user