Files
GUI-Agent/SystemTest-Controller/web/app.js
T

1472 lines
60 KiB
JavaScript

const state = {
currentSection: "overview",
health: null,
agents: [],
tasks: [],
metrics: {},
modelConfig: { value: {}, version: 1 },
siteScan: {
status: "idle",
host: "",
ports: "80,443,3000,5173,8000-8010,8080,8081,9000",
sites: [],
message: "",
},
systemTest: {
targetUrl: "",
assignedAgentId: "",
maxDepth: 2,
status: "idle",
message: "",
},
discovery: {
status: "idle",
targetUrl: "",
planName: "智能探索 P0",
candidates: [],
summary: null,
message: "",
},
toolConfig: { tools: [], config: {}, version: 1 },
report: null,
selectedTaskId: "",
selectedToolAgent: "",
selectedToolPlan: "",
};
const titles = {
overview: "总览",
plans: "系统测试",
console: "运行控制台",
devices: "设备监控",
models: "模型配置",
tools: "Tool 配置",
metrics: "指标看板",
reports: "报告中心",
};
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => Array.from(root.querySelectorAll(selector));
function icon(name) {
return `<svg aria-hidden="true"><use href="#i-${name}"></use></svg>`;
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function formatDate(value) {
if (!value) return "-";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return escapeHtml(value);
return date.toLocaleString();
}
function percent(value) {
const n = Number(value || 0);
return `${n.toFixed(n % 1 ? 1 : 0)}%`;
}
function duration(value) {
if (value == null || value === "") return "-";
const n = Number(value);
if (Number.isNaN(n)) return "-";
return n >= 60 ? `${(n / 60).toFixed(1)} min` : `${n.toFixed(1)} s`;
}
function statusBadge(status) {
const normalized = String(status || "-");
const ok = ["online", "idle", "succeeded", "healthy"].includes(normalized);
const warn = ["ready", "routed", "running", "registered", "claimed"].includes(normalized);
const fail = ["failed", "cancelled", "offline"].includes(normalized);
const cls = ok ? "ok" : fail ? "fail" : warn ? "warn" : "info";
return `<span class="badge ${cls}">${escapeHtml(normalized)}</span>`;
}
function showToast(message) {
const toast = $("#toast");
toast.textContent = message;
toast.hidden = false;
clearTimeout(showToast.timer);
showToast.timer = setTimeout(() => {
toast.hidden = true;
}, 3200);
}
async function fetchJson(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
Accept: "application/json",
...(options.body ? { "Content-Type": "application/json; charset=utf-8" } : {}),
...(options.headers || {}),
},
});
const data = await response.json().catch(() => ({}));
if (!response.ok || data.ok === false) {
throw new Error(data.error || `${response.status} ${response.statusText}`);
}
return data;
}
async function postJson(url, payload = {}) {
return fetchJson(url, { method: "POST", body: JSON.stringify(payload) });
}
async function refreshAll() {
$("#refreshButton").disabled = true;
try {
const [health, dashboard, report] = await Promise.all([
fetchJson("/health"),
fetchJson("/api/dashboard"),
fetchJson("/api/reports/summary"),
]);
state.health = health;
state.agents = dashboard.agents || [];
state.tasks = dashboard.tasks || [];
state.metrics = dashboard.metrics || {};
state.modelConfig = dashboard.model_config || { value: {}, version: 1 };
state.toolConfig = dashboard.tool_config || { tools: [], config: {}, version: 1 };
state.report = report.report;
renderAll();
} catch (error) {
renderHealthError(error);
showToast(error.message);
} finally {
$("#refreshButton").disabled = false;
}
}
function renderHealthError(error) {
const pill = $("#healthPill");
pill.classList.remove("ok");
pill.classList.add("fail");
pill.textContent = "offline";
$("#serviceMeta").textContent = error.message;
}
function renderChrome() {
const health = state.health;
const pill = $("#healthPill");
pill.classList.toggle("ok", Boolean(health?.ok));
pill.classList.toggle("fail", !health?.ok);
pill.textContent = health?.ok ? "healthy" : "attention";
$("#sectionTitle").textContent = titles[state.currentSection];
$("#serviceMeta").textContent = health
? `${health.name} · ${health.public_base_url} · ${health.deployment_mode}`
: "Loading...";
}
function renderAll() {
renderChrome();
renderCurrentView();
}
function renderCurrentView() {
if (state.currentSection === "overview") renderOverview();
if (state.currentSection === "plans") renderPlans();
if (state.currentSection === "console") renderConsole();
if (state.currentSection === "devices") renderDevices();
if (state.currentSection === "models") renderModels();
if (state.currentSection === "tools") renderTools();
if (state.currentSection === "metrics") renderMetrics();
if (state.currentSection === "reports") renderReports();
}
function kpi(label, value, hint) {
return `<div class="kpi"><label>${escapeHtml(label)}</label><strong>${escapeHtml(value)}</strong><span>${escapeHtml(hint)}</span></div>`;
}
function renderOverview() {
const m = state.metrics;
const health = state.health;
$("#view-overview").innerHTML = `
<div class="view-grid">
<div class="kpi-grid">
${kpi("在线被控端", `${m.agent_active || 0}/${m.agent_total || 0}`, "盒子物理机 / 虚拟机")}
${kpi("待执行任务", m.ready_tasks || 0, "ready + routed")}
${kpi("运行中", m.running_tasks || 0, "Agent 占用")}
${kpi("通过率", percent(m.pass_rate), `${m.succeeded_tasks || 0} passed / ${m.failed_tasks || 0} failed`)}
</div>
<div class="view-grid two-col">
<section class="surface">
<div class="surface-header">
<h2>主控健康</h2>
${statusBadge(health?.ok ? "healthy" : "attention")}
</div>
<div class="surface-body">
<dl class="kv">
<dt>数据库</dt><dd>${health?.checks?.database?.ok ? "OK" : "FAIL"}</dd>
<dt>任务数</dt><dd>${escapeHtml(health?.checks?.database?.task_count ?? 0)}</dd>
<dt>端口</dt><dd>${escapeHtml(`${health?.checks?.port?.host || "-"}:${health?.checks?.port?.port || "-"}`)}</dd>
<dt>静态面板</dt><dd>${health?.checks?.static_panel?.ok ? "OK" : "MISSING"}</dd>
<dt>启动时间</dt><dd>${formatDate(health?.started_at)}</dd>
</dl>
</div>
</section>
<section class="surface">
<div class="surface-header">
<h2>最近被控端</h2>
<span class="muted">${state.agents.length} devices</span>
</div>
<div class="surface-body">
${renderAgentMiniList()}
</div>
</section>
</div>
<section class="surface">
<div class="surface-header">
<h2>最近任务</h2>
<button class="button" type="button" data-section-jump="console">${icon("play")}打开控制台</button>
</div>
<div class="table-wrap">${renderTaskTable(state.tasks.slice(0, 8), { actions: false })}</div>
</section>
</div>
`;
}
function renderAgentMiniList() {
if (!state.agents.length) return `<p class="empty">暂无被控端接入</p>`;
return `
<div class="device-grid">
${state.agents.slice(0, 4).map((agent) => `
<article class="device-card">
<header>
<div>
<strong>${escapeHtml(agent.display_name || agent.public_id)}</strong>
<span class="muted">${escapeHtml(agent.deployment_type || "-")}</span>
</div>
${statusBadge(agent.status)}
</header>
<dl class="kv">
<dt>IP</dt><dd>${escapeHtml(agent.reported_ip || agent.reported_host || "-")}</dd>
<dt>日志端口</dt><dd>${escapeHtml(agent.reported_log_web_port || "-")}</dd>
<dt>心跳</dt><dd>${formatDate(agent.last_seen_at)}</dd>
</dl>
</article>
`).join("")}
</div>
`;
}
function renderDiscoveryPanel() {
const discovery = state.discovery;
const cfg = state.modelConfig.value || {};
const targetUrl = discovery.targetUrl || cfg.target_system_url || "";
const selectedCount = (discovery.candidates || []).filter((candidate) => candidate.selected !== false).length;
return `
<section class="surface discovery-panel" style="grid-column:1/-1">
<div class="surface-header">
<h2>智能探索 P0</h2>
<div class="toolbar">
<button class="button" type="button" data-action="discover-site" ${discovery.status === "running" ? "disabled" : ""}>${icon("activity")}发现任务</button>
<button class="button primary" type="button" data-action="import-discovery-tasks" ${selectedCount ? "" : "disabled"}>${icon("upload")}确认入队</button>
</div>
</div>
<div class="surface-body">
<div class="discovery-fields">
<div class="field">
<label for="discoveryTargetUrl">待测系统 URL</label>
<input id="discoveryTargetUrl" type="url" value="${escapeHtml(targetUrl)}" placeholder="http://host:port" />
</div>
<div class="field">
<label for="discoveryPlanName">计划名称</label>
<input id="discoveryPlanName" value="${escapeHtml(discovery.planName)}" />
</div>
</div>
<div id="discoveryResult">${renderDiscoveryResult()}</div>
</div>
</section>
`;
}
function renderDiscoveryResult() {
const discovery = state.discovery;
if (discovery.status === "idle") {
return `<div class="discovery-empty">输入待测系统地址后点击“发现任务”,Agent 会先基于首页结构拆出 P0 子任务。</div>`;
}
if (discovery.status === "running") {
return `<div class="discovery-empty">正在读取页面结构并生成候选任务...</div>`;
}
if (discovery.status === "error") {
return `<div class="discovery-empty fail">${escapeHtml(discovery.message || "发现任务失败")}</div>`;
}
const candidates = discovery.candidates || [];
if (!candidates.length) {
return `<div class="discovery-empty">未发现可入队任务。</div>`;
}
const summary = discovery.summary || {};
const rows = candidates.map((candidate) => {
const depth = Math.max(0, Number(candidate.depth || 0));
const risk = candidate.risk_level || "low";
const selected = candidate.selected !== false;
const preconditions = Array.isArray(candidate.preconditions) && candidate.preconditions.length
? `<span class="discovery-note">前置:${escapeHtml(candidate.preconditions.join("、"))}</span>`
: "";
return `
<label class="discovery-row" style="--depth:${depth}">
<input class="discovery-checkbox" type="checkbox" data-task-id="${escapeHtml(candidate.id)}" ${selected ? "checked" : ""} />
<div class="discovery-main">
<strong>${escapeHtml(candidate.title || candidate.id)}</strong>
<span>${escapeHtml(candidate.objective || "")}</span>
<span class="mono site-url">${escapeHtml(candidate.target_url || "")}</span>
<div class="discovery-meta">
<span class="badge info">${escapeHtml(candidate.level || "case")}</span>
<span class="badge ${risk === "high" ? "fail" : risk === "medium" ? "warn" : "ok"}">${escapeHtml(risk)}</span>
<span>${escapeHtml(candidate.source || "discovery")}</span>
${preconditions}
</div>
</div>
</label>
`;
}).join("");
return `
<div class="discovery-summary">
<span>页面:${escapeHtml(summary.page_title || summary.final_url || summary.target_url || "-")}</span>
<span>候选 ${escapeHtml(summary.candidate_count || candidates.length)} 个</span>
<span>链接 ${escapeHtml(summary.link_count || 0)} / 表单 ${escapeHtml(summary.form_count || 0)} / 入口 ${escapeHtml(summary.button_count || 0)}</span>
<button class="button small" type="button" data-action="toggle-discovery-all">全选/反选</button>
</div>
<div class="discovery-tree">${rows}</div>
`;
}
function activeAgents() {
return state.agents.filter((agent) => ["online", "idle", "running", "registered"].includes(String(agent.status || "")));
}
function systemTestTargetUrl() {
const cfg = state.modelConfig.value || {};
return state.systemTest.targetUrl || cfg.target_system_url || "";
}
function renderSystemTestAgentOptions() {
const selected = String(state.systemTest.assignedAgentId || "");
return [`<option value="">自动分配空闲被控端</option>`]
.concat(state.agents.map((agent) => {
const label = `${agent.display_name || agent.public_id || agent.id} ${agent.reported_ip || ""} ${agent.status || ""}`.trim();
return `<option value="${escapeHtml(agent.id)}" ${String(agent.id) === selected ? "selected" : ""}>${escapeHtml(label)}</option>`;
}))
.join("");
}
function renderSystemTestStatus() {
const activeCount = activeAgents().length;
const status = state.systemTest.status;
const message = state.systemTest.message || (activeCount ? `${activeCount} 个被控端可接收任务` : "暂无在线被控端,任务会先进入队列");
const cls = status === "error" ? "fail" : status === "starting" ? "warn" : activeCount ? "ok" : "info";
return `<div class="system-test-status"><span class="badge ${cls}">${escapeHtml(status)}</span><span>${escapeHtml(message)}</span></div>`;
}
function readSystemTestForm() {
const targetUrl = $("#systemTestTargetUrl")?.value.trim() || "";
const assignedAgentId = $("#systemTestAgent")?.value || "";
const maxDepth = Math.max(1, Math.min(5, Number($("#systemTestMaxDepth")?.value || 2)));
state.systemTest = {
...state.systemTest,
targetUrl,
assignedAgentId,
maxDepth,
};
return { target_url: targetUrl, assigned_agent_id: assignedAgentId || null, max_depth: maxDepth };
}
function renderPlans() {
const targetUrl = systemTestTargetUrl();
$("#view-plans").innerHTML = `
<div class="view-grid">
<section class="surface">
<div class="surface-header">
<h2>系统测试</h2>
<button class="button primary" type="button" data-action="start-system-test" ${state.systemTest.status === "starting" ? "disabled" : ""}>${icon("play")}启动系统测试</button>
</div>
<div class="surface-body field-grid">
<div class="field full">
<label for="systemTestTargetUrl">待测系统 URL</label>
<input id="systemTestTargetUrl" type="url" value="${escapeHtml(targetUrl)}" placeholder="http://host:port" />
</div>
<div class="field">
<label for="systemTestAgent">被控端</label>
<select id="systemTestAgent">${renderSystemTestAgentOptions()}</select>
</div>
<div class="field">
<label for="systemTestMaxDepth">探索深度</label>
<input id="systemTestMaxDepth" type="number" min="1" max="5" value="${escapeHtml(state.systemTest.maxDepth || 2)}" />
</div>
<div class="field full">${renderSystemTestStatus()}</div>
</div>
</section>
<section class="surface">
<div class="surface-header">
<h2>网站扫描</h2>
<span class="muted">IP + Port</span>
</div>
<div class="surface-body">${renderSiteScanner()}</div>
</section>
<section class="surface">
<div class="surface-header">
<h2>被控端</h2>
<span class="muted">${state.agents.length} devices</span>
</div>
<div class="surface-body">${renderAgentMiniList()}</div>
</section>
<section class="surface">
<div class="surface-header">
<h2>任务队列</h2>
<button class="button" type="button" data-section-jump="console">${icon("play")}运行控制台</button>
</div>
<div class="table-wrap">${renderTaskTable(state.tasks, { actions: false })}</div>
</section>
${renderTaskDetail()}
</div>
`;
}
function renderConsole() {
$("#view-console").innerHTML = `
<div class="view-grid">
<div class="kpi-grid">
${kpi("队列", state.metrics.ready_tasks || 0, "ready / routed")}
${kpi("运行", state.metrics.running_tasks || 0, "running")}
${kpi("完成", state.metrics.completed_tasks || 0, "succeeded / failed / cancelled")}
${kpi("并发利用", percent(state.metrics.concurrency_utilization), "running / online agents")}
</div>
<section class="surface">
<div class="surface-header">
<h2>任务队列</h2>
<div class="toolbar">
<select id="taskStatusFilter" class="mini-select">
<option value="">全部状态</option>
${["ready", "routed", "running", "succeeded", "failed", "cancelled"].map((s) => `<option value="${s}">${s}</option>`).join("")}
</select>
<button class="button" type="button" data-action="reload-tasks">${icon("refresh")}刷新任务</button>
</div>
</div>
<div class="table-wrap">${renderTaskTable(state.tasks, { actions: true })}</div>
</section>
${renderTaskDetail()}
</div>
`;
}
function renderTaskMeta(task) {
const level = task.task_level || task.payload?.level || "";
const risk = task.risk_level || task.payload?.risk_level || "";
const depth = Number(task.depth || task.payload?.depth || 0);
const parts = [];
if (level) parts.push(`层级 ${depth} · ${level}`);
if (risk) parts.push(`风险 ${risk}`);
if (task.parent_external_id) parts.push(`父任务 ${task.parent_external_id}`);
return parts.length ? `<span class="task-meta">${escapeHtml(parts.join(" / "))}</span>` : "";
}
function renderTaskTable(tasks, options = { actions: true }) {
if (!tasks.length) return `<table><tbody><tr><td class="empty">暂无任务</td></tr></tbody></table>`;
const showActions = options.actions !== false;
const showDetails = options.details !== false;
const agentOptions = [`<option value="">自动</option>`]
.concat(state.agents.map((a) => `<option value="${a.id}">${escapeHtml(a.display_name || a.public_id)}</option>`))
.join("");
return `
<table>
<thead>
<tr>
<th>Priority</th><th>Task</th><th>Plan</th><th>Status</th><th>Agent</th><th>Duration</th>${showDetails || showActions ? "<th>Actions</th>" : ""}
</tr>
</thead>
<tbody>
${tasks.map((task) => `
<tr>
<td class="mono">${escapeHtml(task.priority)}</td>
<td>
<strong>${escapeHtml(task.title || task.external_id || task.id)}</strong>
<br><span class="muted mono">${escapeHtml(task.external_id || `#${task.id}`)}</span>
${renderTaskMeta(task)}
</td>
<td>${escapeHtml(task.plan_name || "-")}</td>
<td>${statusBadge(task.status)}</td>
<td>${escapeHtml(task.assigned_agent_name || task.assigned_agent_id || "-")}</td>
<td>${duration(task.duration_seconds)}</td>
${showDetails || showActions ? `
<td class="actions">
<div class="split-actions">
${showDetails ? `<button class="icon-button" type="button" title="详情" data-action="show-task-detail" data-task-id="${task.id}">${icon("file")}</button>` : ""}
${showActions ? `
<select class="mini-select" id="assign-${task.id}">${agentOptions}</select>
<button class="icon-button" type="button" title="分派" data-action="assign-task" data-task-id="${task.id}">${icon("users")}</button>
<button class="icon-button" type="button" title="重试" data-action="retry-task" data-task-id="${task.id}">${icon("refresh")}</button>
<button class="icon-button" type="button" title="中止" data-action="abort-task" data-task-id="${task.id}">${icon("square")}</button>
` : ""}
</div>
</td>` : ""}
</tr>
`).join("")}
</tbody>
</table>
`;
}
function selectedTask() {
const selectedId = String(state.selectedTaskId || "");
if (selectedId) {
const task = state.tasks.find((item) => String(item.id) === selectedId);
if (task) return task;
}
return state.tasks.find((item) => item.result) || state.tasks[0] || null;
}
function taskAgent(task) {
if (!task?.assigned_agent_id) return null;
return state.agents.find((agent) => String(agent.id) === String(task.assigned_agent_id)) || null;
}
function screenshotHref(task, imagePath) {
const agent = taskAgent(task);
const host = agent?.reported_ip || agent?.reported_host || "";
const port = agent?.reported_log_web_port || "";
if (!host || !port || !imagePath) return "";
return `http://${host}:${port}/agent/screenshot?path=${encodeURIComponent(imagePath)}`;
}
function renderTaskDetail() {
const task = selectedTask();
if (!task) {
return `<section class="surface"><div class="surface-body"><p class="empty">暂无任务详情</p></div></section>`;
}
if (!task.result) {
const payload = task.payload || {};
const agent = taskAgent(task);
const agentStatus = String(agent?.status || "").toLowerCase();
const likelyInterrupted = task.status === "running" && ["idle", "offline", "registered"].includes(agentStatus);
const progressText = task.status === "running"
? likelyInterrupted
? "被控端已恢复为空闲但任务没有结果回传,可能执行中断;请重试该任务。"
: "被控端已领取任务,正在等待执行结果回传。"
: task.status === "ready"
? "任务已进入队列,等待空闲被控端领取。"
: task.status === "cancelled"
? "任务已取消,未产生执行结果。"
: "任务尚未产生执行结果。";
return `
<section class="surface task-detail-panel">
<div class="surface-header">
<h2>任务详情</h2>
${statusBadge(task.status)}
</div>
<div class="surface-body task-detail">
<div class="task-detail-title">
<strong>${escapeHtml(task.title || task.external_id || task.id)}</strong>
<span class="muted mono">${escapeHtml(task.external_id || `#${task.id}`)}</span>
</div>
<dl class="kv">
<dt>目标 URL</dt><dd class="mono">${escapeHtml(payload.target_url || payload.url || "-")}</dd>
<dt>被控端</dt><dd>${escapeHtml(agent?.display_name || task.assigned_agent_name || "-")}</dd>
<dt>执行状态</dt><dd>${escapeHtml(progressText)}</dd>
<dt>创建时间</dt><dd>${formatDate(task.created_at)}</dd>
<dt>领取时间</dt><dd>${formatDate(task.claimed_at)}</dd>
<dt>更新时间</dt><dd>${formatDate(task.updated_at)}</dd>
<dt>执行结论</dt><dd>${escapeHtml(task.last_error || "-")}</dd>
</dl>
<div>
<h3>模型分析</h3>
<p class="muted">任务结果尚未回传,暂时没有模型调用记录。</p>
</div>
<div>
<h3>日志</h3>
<p class="muted">任务结果尚未回传,暂无执行日志。</p>
</div>
</div>
</section>
`;
}
const result = task.result || {};
const observation = result.observation || {};
const browserOpen = result.browser_open || {};
const modelAnalysis = result.model_analysis || {};
const modelUsed = Boolean(modelAnalysis.model_used || result.host_decision?.model_used || result.app_decision?.model_used);
const modelErrors = [...new Set([
...(Array.isArray(result.model_errors) ? result.model_errors : []),
modelAnalysis.error,
result.host_decision?.model_error,
result.app_decision?.model_error,
].filter(Boolean).map((item) => String(item)))];
const modelCallAttempted = Boolean(
modelUsed
|| modelErrors.length
|| modelAnalysis.model_called
|| modelAnalysis.model_duration_seconds
|| result.host_decision?.model_duration_seconds
|| result.app_decision?.model_duration_seconds
);
const modelState = !result.model_available
? "未配置"
: modelUsed
? "已调用"
: modelErrors.length
? "调用失败"
: modelCallAttempted
? "已调用但无有效结果"
: "未调用";
const discoverySource = result.discovery_source === "llm" ? "大模型生成" : result.discovery_source === "rule" ? "规则回退" : "-";
const screenshots = Array.isArray(result.screenshots) ? result.screenshots : [];
const submitted = Array.isArray(result.submitted_child_tasks) ? result.submitted_child_tasks : [];
const discovered = Array.isArray(result.discovered_child_tasks) ? result.discovered_child_tasks : [];
const logs = Array.isArray(result.logs) ? result.logs : [];
const needsUserInput = result.needs_user_input || task.status === "failed";
return `
<section class="surface task-detail-panel">
<div class="surface-header">
<h2>任务详情</h2>
${statusBadge(task.status)}
</div>
<div class="surface-body task-detail">
<div class="task-detail-title">
<strong>${escapeHtml(task.title || task.external_id || task.id)}</strong>
<span class="muted mono">${escapeHtml(task.external_id || `#${task.id}`)}</span>
</div>
<dl class="kv">
<dt>目标 URL</dt><dd class="mono">${escapeHtml(result.target_url || observation.url || (task.payload || {}).target_url || "-")}</dd>
<dt>最终 URL</dt><dd class="mono">${escapeHtml(result.final_url || observation.final_url || "-")}</dd>
<dt>页面标题</dt><dd>${escapeHtml(result.page_title || observation.page_title || "-")}</dd>
<dt>HTTP 状态</dt><dd>${escapeHtml(result.http_status || observation.http_status || "-")}</dd>
<dt>浏览器打开</dt><dd>${browserOpen.opened ? "已打开" : result.browser_open ? "未打开" : "-"}</dd>
<dt>大模型</dt><dd>${escapeHtml(modelState)}${result.model_duration_seconds ? ` · ${duration(result.model_duration_seconds)}` : ""}</dd>
<dt>任务来源</dt><dd>${escapeHtml(discoverySource)}</dd>
<dt>耗时</dt><dd>${duration(result.duration_seconds || task.duration_seconds)}</dd>
<dt>人工介入</dt><dd>${needsUserInput ? '<span class="badge warn">需要确认</span>' : '<span class="badge ok">无需</span>'}</dd>
<dt>执行结论</dt><dd>${escapeHtml(result.message || task.last_error || "-")}</dd>
</dl>
<div>
<h3>模型分析</h3>
${modelUsed ? `
<ul class="detail-list">
<li>${escapeHtml(modelAnalysis.summary || "模型已参与分析,但未返回摘要。")}</li>
${(Array.isArray(modelAnalysis.coverage_notes) ? modelAnalysis.coverage_notes : []).slice(0, 5).map((note) => `<li>${escapeHtml(note)}</li>`).join("")}
${modelAnalysis.user_input_reason ? `<li>需要用户补充:${escapeHtml(modelAnalysis.user_input_reason)}</li>` : ""}
</ul>
` : modelErrors.length ? `
<ul class="detail-list">
${modelErrors.map((error) => `<li>调用失败:${escapeHtml(error)}</li>`).join("")}
</ul>
<p class="muted">本轮已回退到规则探索。</p>
` : `<p class="muted">${result.model_available ? "本轮未使用模型生成有效分析,已回退到规则探索。" : "未配置可用模型,本轮使用规则探索。"}</p>`}
</div>
<div class="task-detail-grid">
<div>
<h3>页面发现</h3>
<ul class="detail-list">
<li>链接:${escapeHtml(observation.link_count ?? "-")}</li>
<li>表单:${escapeHtml(observation.form_count ?? "-")}</li>
<li>按钮:${escapeHtml(observation.button_count ?? "-")}</li>
<li>登录表单:${observation.has_login_form ? "是" : "否"}</li>
</ul>
</div>
<div>
<h3>证据截图</h3>
${screenshots.length ? `
<ul class="detail-list">
${screenshots.map((shot) => {
const imagePath = shot.image_path || shot.path || "";
const href = screenshotHref(task, imagePath);
const label = imagePath || JSON.stringify(shot);
return `<li class="mono">${href ? `<a href="${escapeHtml(href)}" target="_blank" rel="noreferrer">${escapeHtml(label)}</a>` : escapeHtml(label)}</li>`;
}).join("")}
</ul>
` : `<p class="muted">暂无截图</p>`}
</div>
</div>
<div class="task-detail-grid">
<div>
<h3>已加入队列的子任务</h3>
${submitted.length ? `
<ul class="detail-list">
${submitted.map((child) => `<li><strong>${escapeHtml(child.title || child.external_id)}</strong><span class="muted">层级 ${escapeHtml(child.depth ?? "-")} · ${escapeHtml(child.task_level || "-")} · ${escapeHtml(child.source || discoverySource || "-")}</span>${child.llm_reason ? `<span>${escapeHtml(child.llm_reason)}</span>` : ""}</li>`).join("")}
</ul>
` : `<p class="muted">暂无子任务</p>`}
</div>
<div>
<h3>发现候选</h3>
${discovered.length ? `
<ul class="detail-list">
${discovered.slice(0, 8).map((child) => `<li><strong>${escapeHtml(child.title || child.external_id)}</strong><span class="muted mono">${escapeHtml(child.target_url || "-")}</span></li>`).join("")}
</ul>
` : `<p class="muted">暂无候选</p>`}
</div>
</div>
<div>
<h3>日志</h3>
${logs.length ? `<ul class="detail-list logs">${logs.slice(-8).map((line) => `<li>${escapeHtml(line)}</li>`).join("")}</ul>` : `<p class="muted">暂无日志</p>`}
</div>
</div>
</section>
`;
}
function renderDevices() {
$("#view-devices").innerHTML = `
<div class="view-grid">
<section class="surface">
<div class="surface-header">
<h2>监控墙</h2>
<span class="muted">${state.agents.length} agents</span>
</div>
<div class="surface-body">
${state.agents.length ? `
<div class="device-grid">
${state.agents.map((agent) => `
<article class="device-card">
<header>
<div>
<strong>${escapeHtml(agent.display_name || agent.public_id)}</strong>
<span class="muted mono">${escapeHtml(agent.public_id)}</span>
</div>
${statusBadge(agent.status)}
</header>
<dl class="kv">
<dt>部署形态</dt><dd>${escapeHtml(agent.deployment_type || "-")}</dd>
<dt>Agent 版本</dt><dd>${escapeHtml(agent.agent_version || "-")}</dd>
<dt>Host</dt><dd>${escapeHtml(agent.reported_host || "-")}</dd>
<dt>IP</dt><dd>${escapeHtml(agent.reported_ip || "-")}</dd>
<dt>配置版本</dt><dd>v${escapeHtml(agent.config_revision || 0)}${agent.config_applied_at ? ` · ${formatDate(agent.config_applied_at)}` : ""}</dd>
<dt>日志页</dt><dd>${agent.reported_ip && agent.reported_log_web_port ? `<a href="http://${escapeHtml(agent.reported_ip)}:${escapeHtml(agent.reported_log_web_port)}/" target="_blank" rel="noreferrer">打开 ${escapeHtml(agent.reported_log_web_port)}</a>` : "-"}</dd>
<dt>最后心跳</dt><dd>${formatDate(agent.last_seen_at)}</dd>
</dl>
</article>
`).join("")}
</div>` : `<p class="empty">暂无被控端接入</p>`}
</div>
</section>
</div>
`;
}
function renderModels() {
const cfg = state.modelConfig.value || {};
const targetVersion = Number(state.modelConfig.version || 1);
const syncedAgents = state.agents.filter((agent) => Number(agent.config_revision || 0) >= targetVersion).length;
const syncRows = state.agents.length
? state.agents.map((agent) => {
const revision = Number(agent.config_revision || 0);
const isSynced = revision >= targetVersion;
return `
<tr>
<td>
<strong>${escapeHtml(agent.display_name || agent.public_id)}</strong>
<div class="muted mono">${escapeHtml(agent.reported_ip || agent.public_id)}</div>
</td>
<td>v${escapeHtml(revision)}</td>
<td>${isSynced ? '<span class="badge ok">synced</span>' : '<span class="badge warn">pending</span>'}</td>
<td>${formatDate(agent.config_applied_at)}</td>
</tr>
`;
}).join("")
: `<tr><td colspan="4" class="empty-cell">暂无被控端接入</td></tr>`;
$("#view-models").innerHTML = `
<div class="view-grid two-col">
<section class="surface">
<div class="surface-header">
<h2>模型与执行参数</h2>
<span class="badge info">v${escapeHtml(state.modelConfig.version || 1)}</span>
</div>
<div class="surface-body field-grid">
${modelInput("target_system_url", "待测系统 URL", cfg.target_system_url, "url", { placeholder: "http://host:port" })}
${renderSiteScanner()}
${modelInput("vllm_base_url", "vLLM / OpenAI Base URL", cfg.vllm_base_url, "url", { placeholder: "http://host:8000/v1" })}
${modelInput("vllm_model", "模型名称", cfg.vllm_model, "text", { placeholder: "qwen2.5-vl / local-model" })}
<div class="field">
<label for="api_key">API Key <span class="muted">${cfg.api_key_set ? "已保存" : "未保存"}</span></label>
<input id="api_key" type="password" value="" placeholder="${cfg.api_key_set ? "留空保持已保存密钥" : "输入 Bearer Token / API Key"}" autocomplete="off" />
</div>
${modelInput("grounding_url", "Grounding URL", cfg.grounding_url, "url", { placeholder: "http://host:port" })}
${modelInput("grounding_model", "Grounding 模型", cfg.grounding_model, "text", { placeholder: "grounding-model" })}
${modelSelect("ground_coordinate_mode", "坐标模式", cfg.ground_coordinate_mode || "absolute", [["absolute", "absolute"], ["normalized", "normalized"]])}
${modelInput("resolution", "分辨率", cfg.resolution, "text", { placeholder: "1920x1080" })}
${modelInput("screenshot_max_width", "截图最大宽", cfg.screenshot_max_width, "number", { min: 1, max: 7680 })}
${modelInput("screenshot_max_height", "截图最大高", cfg.screenshot_max_height, "number", { min: 1, max: 7680 })}
${modelInput("max_steps", "最大步数", cfg.max_steps, "number", { min: 1, max: 500 })}
${modelInput("request_timeout_seconds", "请求超时秒", cfg.request_timeout_seconds, "number", { min: 1, max: 300 })}
${modelInput("temperature", "Temperature", cfg.temperature, "number", { min: 0, max: 2, step: "0.1" })}
<div class="field checkbox-field">
<label><input id="clear_api_key" type="checkbox" /> 清除已保存 API Key</label>
</div>
<div class="field full">
<div class="toolbar">
<button class="button primary" type="button" data-action="save-model">${icon("save")}保存配置</button>
<button class="button" type="button" data-action="test-model">${icon("activity")}连通性测试</button>
</div>
<p class="form-hint">保存后版本递增,并通过被控端心跳下发;被控端应用新配置后会重启运行时。</p>
</div>
</div>
</section>
<div class="view-grid">
<section class="surface">
<div class="surface-header">
<h2>配置同步</h2>
<span class="muted">${syncedAgents}/${state.agents.length} agents</span>
</div>
<div class="table-wrap compact-table">
<table>
<thead><tr><th>Agent</th><th>版本</th><th>状态</th><th>应用时间</th></tr></thead>
<tbody>${syncRows}</tbody>
</table>
</div>
</section>
<section class="surface">
<div class="surface-header"><h2>测试结果</h2></div>
<div class="surface-body">
<div id="modelTestResult">${renderModelTestResult(null)}</div>
</div>
</section>
</div>
</div>
`;
}
function renderSiteScanner() {
const scan = state.siteScan;
return `
<div class="field full site-scan-panel">
<div class="site-scan-fields">
<div class="field">
<label for="site_scan_host">扫描主机</label>
<input id="site_scan_host" type="text" value="${escapeHtml(scan.host)}" placeholder="local / 127.0.0.1 / 10.168.1.20" />
</div>
<div class="field">
<label for="site_scan_ports">扫描端口</label>
<input id="site_scan_ports" type="text" value="${escapeHtml(scan.ports)}" placeholder="80,443,8000-8010" />
</div>
<div class="field site-scan-action">
<button class="button" type="button" data-action="scan-sites" ${scan.status === "running" ? "disabled" : ""}>${icon("refresh")}扫描网站</button>
</div>
</div>
<div id="siteScanResult">${renderSiteScanResult()}</div>
</div>
`;
}
function renderSiteScanResult() {
const scan = state.siteScan;
if (scan.status === "idle") {
return `<div class="site-scan-empty">尚未扫描</div>`;
}
if (scan.status === "running") {
return `<div class="site-scan-empty">正在扫描可访问网站...</div>`;
}
if (scan.status === "error") {
return `<div class="site-scan-empty fail">${escapeHtml(scan.message || "扫描失败")}</div>`;
}
const sites = scan.sites || [];
if (!sites.length) {
return `<div class="site-scan-empty">未发现可访问网站</div>`;
}
const rows = sites.map((site) => {
const address = site.address || `${site.ip || site.host}:${site.port}`;
const statusText = site.status ? `HTTP ${site.status}` : "HTTP";
const elapsed = site.elapsed_seconds != null ? `${Number(site.elapsed_seconds).toFixed(2)}s` : "-";
return `
<div class="site-result-row">
<div class="site-result-main">
<strong>${escapeHtml(site.name || address)}</strong>
<span class="site-url mono">${escapeHtml(site.url || "")}</span>
<span class="site-meta">${escapeHtml(address)} · ${escapeHtml(statusText)} · ${escapeHtml(elapsed)}</span>
</div>
<button class="button small" type="button" data-action="select-site" data-site-url="${escapeHtml(site.url || "")}">${icon("check")}选择</button>
</div>
`;
}).join("");
return `
<div class="site-scan-summary">发现 ${sites.length} 个可访问网站</div>
<div class="site-result-list">${rows}</div>
`;
}
function renderModelTestResult(test) {
if (!test) {
return `
<div class="test-result neutral">
<div class="test-result-icon">${icon("activity")}</div>
<div>
<h3>尚未测试</h3>
<p>点击“连通性测试”后,这里会显示模型服务是否可访问、响应耗时和发现的模型。</p>
</div>
</div>
`;
}
if (test.status === "running") {
return `
<div class="test-result neutral">
<div class="test-result-icon">${icon("activity")}</div>
<div>
<h3>正在测试模型服务</h3>
<p>正在访问模型服务,请稍候。</p>
</div>
</div>
`;
}
if (!test.ok) {
return `
<div class="test-result fail">
<div class="test-result-icon">${icon("x")}</div>
<div>
<h3>连接失败</h3>
<p>${escapeHtml(test.message || "模型服务暂时不可访问。")}</p>
${test.url ? `<dl class="kv compact-kv"><dt>测试地址</dt><dd>${escapeHtml(test.url)}</dd></dl>` : ""}
<ul class="hint-list">
<li>确认模型服务已启动,并且主控主机可以访问该地址。</li>
<li>检查 Base URL 是否包含正确端口,例如 http://host:8000/v1。</li>
<li>如果服务需要鉴权,请确认 API Key 已保存且未过期。</li>
</ul>
</div>
</div>
`;
}
const models = normalizeModelList(test.models);
const modelList = models.length
? `<div class="model-chip-list">${models.map((name) => `<span class="model-chip">${escapeHtml(name)}</span>`).join("")}</div>`
: `<p class="muted">接口可访问,但没有返回可展示的模型名称。</p>`;
return `
<div class="test-result ok">
<div class="test-result-icon">${icon("check")}</div>
<div>
<h3>连接成功</h3>
<p>模型服务已响应,可以用于后续测试执行。</p>
<dl class="kv compact-kv">
<dt>测试地址</dt><dd>${escapeHtml(test.url || "-")}</dd>
<dt>响应耗时</dt><dd>${duration(test.elapsed_seconds)}</dd>
</dl>
<h3 class="subheading">发现的模型</h3>
${modelList}
</div>
</div>
`;
}
function normalizeModelList(models) {
if (!Array.isArray(models)) return [];
return models
.map((item) => {
if (typeof item === "string") return item;
if (!item || typeof item !== "object") return "";
return item.id || item.name || item.model || item.object || "";
})
.filter(Boolean)
.slice(0, 12);
}
function modelInput(id, label, value, type = "text", attrs = {}) {
const attrText = Object.entries(attrs)
.filter(([key]) => key !== "full")
.map(([key, val]) => `${key}="${escapeHtml(val)}"`)
.join(" ");
return `<div class="field${attrs.full ? " full" : ""}"><label for="${id}">${escapeHtml(label)}</label><input id="${id}" type="${type}" value="${escapeHtml(value ?? "")}" ${attrText} /></div>`;
}
function modelSelect(id, label, value, options) {
return `
<div class="field">
<label for="${id}">${escapeHtml(label)}</label>
<select id="${id}">
${options.map(([optionValue, labelText]) => `<option value="${escapeHtml(optionValue)}" ${optionValue === value ? "selected" : ""}>${escapeHtml(labelText)}</option>`).join("")}
</select>
</div>
`;
}
function readModelForm() {
const fields = [
"target_system_url",
"vllm_base_url",
"vllm_model",
"api_key",
"grounding_url",
"grounding_model",
"ground_coordinate_mode",
"resolution",
"screenshot_max_width",
"screenshot_max_height",
"max_steps",
"request_timeout_seconds",
"temperature",
];
const payload = {};
fields.forEach((field) => {
const node = $(`#${field}`);
payload[field] = node.type === "number" ? Number(node.value || 0) : node.value.trim();
});
payload.clear_api_key = Boolean($("#clear_api_key")?.checked);
return payload;
}
function readSiteScanForm() {
const host = $("#site_scan_host")?.value.trim() || "";
const ports = $("#site_scan_ports")?.value.trim() || "";
state.siteScan.host = host;
state.siteScan.ports = ports || state.siteScan.ports;
return { host, ports, timeout_seconds: 0.8 };
}
function readDiscoveryForm() {
const targetUrl = $("#discoveryTargetUrl")?.value.trim() || "";
const planName = $("#discoveryPlanName")?.value.trim() || "智能探索 P0";
state.discovery.targetUrl = targetUrl;
state.discovery.planName = planName;
return { target_url: targetUrl, plan_name: planName, timeout_seconds: 8 };
}
function selectedDiscoveryCandidates() {
const checked = new Set($$(".discovery-checkbox:checked").map((node) => node.dataset.taskId));
state.discovery.candidates = (state.discovery.candidates || []).map((candidate) => ({
...candidate,
selected: checked.has(String(candidate.id)),
}));
return state.discovery.candidates.filter((candidate) => candidate.selected);
}
function renderTools() {
const tools = state.toolConfig.tools || [];
const config = state.toolConfig.config || {};
const globalEnabled = config.global_enabled || {};
const agentOptions = [`<option value="">选择 Agent</option>`]
.concat(state.agents.map((a) => `<option value="${a.id}" ${String(a.id) === String(state.selectedToolAgent) ? "selected" : ""}>${escapeHtml(a.display_name || a.public_id)}</option>`))
.join("");
const agentOverride = (config.agent_overrides || {})[state.selectedToolAgent] || {};
const planOverride = (config.plan_overrides || {})[state.selectedToolPlan] || {};
$("#view-tools").innerHTML = `
<div class="view-grid two-col">
<section class="surface">
<div class="surface-header">
<h2>全局默认</h2>
<span class="badge info">v${escapeHtml(state.toolConfig.version || 1)}</span>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>Enabled</th><th>Tool</th><th>Type</th><th>Source</th><th>Scenario</th></tr></thead>
<tbody>
${tools.map((tool) => `
<tr>
<td><input type="checkbox" data-tool-global="${escapeHtml(tool.id)}" ${globalEnabled[tool.id] !== false ? "checked" : ""} /></td>
<td><strong>${escapeHtml(tool.name)}</strong><br><span class="muted">${escapeHtml(tool.capability)}</span></td>
<td>${escapeHtml(tool.type)}</td>
<td>${escapeHtml(tool.source)} · ${escapeHtml(tool.version)}</td>
<td>${escapeHtml(tool.scenario)}</td>
</tr>
`).join("")}
</tbody>
</table>
</div>
</section>
<section class="surface">
<div class="surface-header">
<h2>覆盖策略</h2>
<button class="button primary" type="button" data-action="save-tools">${icon("save")}保存策略</button>
</div>
<div class="surface-body view-grid">
<div class="field">
<label for="toolAgentSelect">Agent 级覆盖</label>
<select id="toolAgentSelect">${agentOptions}</select>
</div>
<div class="toolbar">
${tools.map((tool) => `<label class="badge"><input type="checkbox" data-tool-agent="${escapeHtml(tool.id)}" ${agentOverride[tool.id] !== false ? "checked" : ""} /> ${escapeHtml(tool.name)}</label>`).join("")}
</div>
<div class="field">
<label for="toolPlanName">计划级覆盖</label>
<input id="toolPlanName" value="${escapeHtml(state.selectedToolPlan)}" placeholder="Plan name" />
</div>
<div class="toolbar">
${tools.map((tool) => `<label class="badge"><input type="checkbox" data-tool-plan="${escapeHtml(tool.id)}" ${planOverride[tool.id] !== false ? "checked" : ""} /> ${escapeHtml(tool.name)}</label>`).join("")}
</div>
</div>
</section>
</div>
`;
}
function renderMetrics() {
const m = state.metrics;
const rows = [
["通过率", m.pass_rate],
["失败率", m.fail_rate],
["覆盖率", m.coverage_rate],
["并发利用率", m.concurrency_utilization],
["人工介入率", m.manual_intervention_rate],
["异常可恢复率", m.recovery_rate],
["Tool 配置一致率", m.tool_consistency_rate],
];
$("#view-metrics").innerHTML = `
<div class="view-grid">
<div class="kpi-grid">
${kpi("任务总数", m.total_tasks || 0, "all plans")}
${kpi("平均耗时", duration(m.avg_duration_seconds), "completed tasks")}
${kpi("通过", m.succeeded_tasks || 0, "succeeded")}
${kpi("失败", m.failed_tasks || 0, "failed")}
</div>
<section class="surface">
<div class="surface-header">
<h2>核心指标</h2>
<span class="muted">${(m.plans || []).join(", ") || "No plans"}</span>
</div>
<div class="surface-body metric-list">
${rows.map(([label, value]) => `
<div class="metric-row">
<span>${escapeHtml(label)}</span>
<div class="progress-bar"><span style="width:${Math.max(0, Math.min(100, Number(value || 0)))}%"></span></div>
<strong>${percent(value)}</strong>
</div>
`).join("")}
</div>
</section>
</div>
`;
}
function renderReports() {
const report = state.report || { metrics: {}, plans: [], failed_tasks: [] };
$("#view-reports").innerHTML = `
<div class="view-grid two-col">
<section class="surface">
<div class="surface-header">
<h2>报告摘要</h2>
<button class="button" type="button" data-action="download-report">${icon("download")}下载 JSON</button>
</div>
<div class="surface-body">
<dl class="kv">
<dt>生成时间</dt><dd>${formatDate(report.generated_at)}</dd>
<dt>总任务</dt><dd>${escapeHtml(report.metrics?.total_tasks || 0)}</dd>
<dt>通过率</dt><dd>${percent(report.metrics?.pass_rate)}</dd>
<dt>覆盖率</dt><dd>${percent(report.metrics?.coverage_rate)}</dd>
</dl>
</div>
</section>
<section class="surface">
<div class="surface-header"><h2>失败清单</h2></div>
<div class="table-wrap">
<table>
<thead><tr><th>Task</th><th>Plan</th><th>Reason</th></tr></thead>
<tbody>
${(report.failed_tasks || []).length ? report.failed_tasks.map((task) => `
<tr><td>${escapeHtml(task.title || task.external_id || task.id)}</td><td>${escapeHtml(task.plan_name || "-")}</td><td>${escapeHtml(task.reason || "-")}</td></tr>
`).join("") : `<tr><td colspan="3" class="empty">暂无失败任务</td></tr>`}
</tbody>
</table>
</div>
</section>
</div>
`;
}
function estimateTasks(raw) {
if (Array.isArray(raw)) return raw.reduce((sum, item) => sum + (item && Array.isArray(item.tasks) ? item.tasks.length : 1), 0);
if (raw && Array.isArray(raw.plans)) return raw.plans.reduce((sum, plan) => sum + (Array.isArray(plan.tasks) ? plan.tasks.length : 0), 0);
if (raw && Array.isArray(raw.tasks)) return raw.tasks.length;
if (raw && raw.tasks && typeof raw.tasks === "object") return Object.keys(raw.tasks).length;
return raw && typeof raw === "object" ? 1 : 0;
}
function samplePlan() {
return JSON.stringify(
{
plan_name: "ERP Smoke Plan",
tasks: [
{
task_id: "ERP-LOGIN-001",
title: "登录系统",
priority: 10,
tags: ["login", "smoke"],
instruction: "打开待测系统,使用测试账号登录,确认首页可访问。",
timeout: 300,
task_failure_policy: "terminate",
},
{
task_id: "ERP-QUERY-001",
title: "查询采购单",
priority: 20,
tags: ["purchase"],
instruction: "进入采购模块,按单号查询并确认列表展示结果。",
timeout: 300,
task_failure_policy: "skip",
},
],
},
null,
2
);
}
function downloadJson(filename, value) {
const blob = new Blob([JSON.stringify(value, null, 2)], { type: "application/json" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
document.addEventListener("click", async (event) => {
const nav = event.target.closest(".nav-item");
if (nav) {
setSection(nav.dataset.section);
return;
}
const jump = event.target.closest("[data-section-jump]");
if (jump) {
setSection(jump.dataset.sectionJump);
return;
}
const button = event.target.closest("[data-action]");
if (!button) return;
const action = button.dataset.action;
try {
if (action === "start-system-test") {
const form = readSystemTestForm();
if (!form.target_url) {
state.systemTest.status = "error";
state.systemTest.message = "请先选择或填写待测系统 URL";
renderPlans();
return;
}
state.systemTest.status = "starting";
state.systemTest.message = "正在创建根任务";
renderPlans();
const result = await postJson("/api/system-tests/start", form);
state.systemTest.status = "started";
state.systemTest.message = `已创建根任务:${result.task?.external_id || result.task?.id || "-"}`;
showToast("系统测试已启动,等待被控端领取任务");
setSection("console");
await refreshAll();
} else if (action === "sample-plan") {
$("#planJson").value = samplePlan();
$("#planValidation").textContent = "已填入样例";
} else if (action === "validate-plan") {
const raw = JSON.parse($("#planJson").value || "{}");
$("#planValidation").textContent = `JSON 有效,可导入 ${estimateTasks(raw)} 条任务`;
} else if (action === "import-plan") {
const content = $("#planJson").value;
const result = await postJson("/api/tasks/import", { content });
showToast(`已导入 ${result.count} 条任务`);
await refreshAll();
} else if (action === "discover-site") {
const form = readDiscoveryForm();
const resultContainer = $("#discoveryResult");
state.discovery.status = "running";
state.discovery.message = "";
state.discovery.candidates = [];
state.discovery.summary = null;
if (resultContainer) resultContainer.innerHTML = renderDiscoveryResult();
button.disabled = true;
try {
const result = await postJson("/api/discovery/preview", form);
state.discovery.status = "done";
state.discovery.summary = result.summary || null;
state.discovery.candidates = result.candidates || [];
state.discovery.message = "";
renderPlans();
showToast(`发现 ${state.discovery.candidates.length} 个候选任务`);
} catch (error) {
state.discovery.status = "error";
state.discovery.message = error.message;
if (resultContainer) resultContainer.innerHTML = renderDiscoveryResult();
} finally {
button.disabled = false;
}
} else if (action === "toggle-discovery-all") {
const boxes = $$(".discovery-checkbox");
const shouldCheck = boxes.some((box) => !box.checked);
boxes.forEach((box) => {
box.checked = shouldCheck;
});
} else if (action === "import-discovery-tasks") {
const form = readDiscoveryForm();
const tasks = selectedDiscoveryCandidates();
if (!tasks.length) {
showToast("请先选择要入队的任务");
return;
}
const result = await postJson("/api/discovery/import", {
target_url: form.target_url,
plan_name: form.plan_name,
tasks,
});
showToast(`已入队 ${result.count} 个智能探索任务`);
await refreshAll();
} else if (action === "export-tasks") {
downloadJson("system-test-tasks.json", state.tasks);
} else if (action === "create-task") {
await postJson("/api/tasks", {
task_id: $("#manualTaskId").value.trim(),
title: $("#manualTitle").value.trim(),
plan_name: $("#manualPlan").value.trim(),
priority: Number($("#manualPriority").value || 100),
failure_policy: $("#manualPolicy").value,
instruction: $("#manualInstruction").value.trim(),
});
showToast("任务已保存");
await refreshAll();
} else if (action === "reload-tasks") {
const status = $("#taskStatusFilter")?.value;
const data = await fetchJson(status ? `/api/tasks?status=${encodeURIComponent(status)}` : "/api/tasks");
state.tasks = data.tasks || [];
renderConsole();
} else if (action === "show-task-detail") {
state.selectedTaskId = button.dataset.taskId || "";
if (state.currentSection === "console") {
renderConsole();
} else {
renderPlans();
}
} else if (action === "assign-task") {
const taskId = button.dataset.taskId;
const agentId = $(`#assign-${taskId}`).value;
await postJson(`/api/tasks/${taskId}/assign`, { agent_id: agentId || null, reason: "manual dispatch from console" });
showToast("分派已保存");
await refreshAll();
} else if (action === "retry-task") {
await postJson(`/api/tasks/${button.dataset.taskId}/retry`, { reason: "retry from console" });
showToast("任务已回到队列");
await refreshAll();
} else if (action === "abort-task") {
await postJson(`/api/tasks/${button.dataset.taskId}/abort`, { reason: "aborted from console" });
showToast("任务已中止");
await refreshAll();
} else if (action === "save-model") {
const saved = await postJson("/api/config/model", readModelForm());
state.modelConfig = saved;
showToast(`模型配置已保存 v${saved.version}`);
await refreshAll();
} else if (action === "test-model") {
const container = $("#modelTestResult");
container.innerHTML = renderModelTestResult({ status: "running" });
try {
const result = await postJson("/api/models/test", readModelForm());
container.innerHTML = renderModelTestResult(result.test);
} catch (error) {
container.innerHTML = renderModelTestResult({ ok: false, message: error.message });
}
} else if (action === "scan-sites") {
const resultContainer = $("#siteScanResult");
button.disabled = true;
state.siteScan.status = "running";
state.siteScan.message = "";
state.siteScan.sites = [];
resultContainer.innerHTML = renderSiteScanResult();
try {
const result = await postJson("/api/sites/scan", readSiteScanForm());
state.siteScan.status = "done";
state.siteScan.sites = result.sites || [];
state.siteScan.message = "";
resultContainer.innerHTML = renderSiteScanResult();
showToast(`发现 ${state.siteScan.sites.length} 个可访问网站`);
} catch (error) {
state.siteScan.status = "error";
state.siteScan.message = error.message;
resultContainer.innerHTML = renderSiteScanResult();
} finally {
button.disabled = false;
}
} else if (action === "select-site") {
const url = button.dataset.siteUrl || "";
state.systemTest.targetUrl = url;
const systemTestInput = $("#systemTestTargetUrl");
if (systemTestInput && url) {
systemTestInput.value = url;
}
const targetInput = $("#target_system_url");
if (targetInput && url) {
targetInput.value = url;
}
if (url) showToast("已填入待测系统 URL");
} else if (action === "save-tools") {
await saveTools();
showToast("Tool 策略已保存");
await refreshAll();
} else if (action === "download-report") {
downloadJson("system-test-report-summary.json", state.report || {});
}
} catch (error) {
showToast(error.message);
}
});
document.addEventListener("change", (event) => {
if (event.target.id === "toolAgentSelect") {
state.selectedToolAgent = event.target.value;
renderTools();
}
});
document.addEventListener("input", (event) => {
if (event.target.id === "toolPlanName") {
state.selectedToolPlan = event.target.value.trim();
}
});
$("#refreshButton").addEventListener("click", refreshAll);
function setSection(section) {
state.currentSection = section;
$$(".nav-item").forEach((item) => item.classList.toggle("is-active", item.dataset.section === section));
$$(".view").forEach((view) => view.classList.toggle("is-active", view.id === `view-${section}`));
renderChrome();
renderCurrentView();
}
async function saveTools() {
const current = state.toolConfig.config || {};
const config = {
global_enabled: { ...(current.global_enabled || {}) },
agent_overrides: { ...(current.agent_overrides || {}) },
plan_overrides: { ...(current.plan_overrides || {}) },
};
$$("[data-tool-global]").forEach((node) => {
config.global_enabled[node.dataset.toolGlobal] = node.checked;
});
if (state.selectedToolAgent) {
config.agent_overrides[state.selectedToolAgent] = {};
$$("[data-tool-agent]").forEach((node) => {
config.agent_overrides[state.selectedToolAgent][node.dataset.toolAgent] = node.checked;
});
}
const planName = $("#toolPlanName")?.value.trim();
if (planName) {
config.plan_overrides[planName] = {};
$$("[data-tool-plan]").forEach((node) => {
config.plan_overrides[planName][node.dataset.toolPlan] = node.checked;
});
state.selectedToolPlan = planName;
}
const saved = await postJson("/api/tools", { tools: state.toolConfig.tools, config });
state.toolConfig = saved;
}
refreshAll();
setInterval(() => {
if (!["plans", "models", "tools"].includes(state.currentSection)) {
refreshAll();
}
}, 5000);