Files
webtest-agent/static/js/app.js
T

221 lines
8.3 KiB
JavaScript
Raw Normal View History

/* webtest-agent 前端逻辑 */
const $ = s => document.querySelector(s);
const API = '';
async function refreshHealth() {
try {
const r = await fetch(API + '/health');
const d = await r.json();
$('#health').textContent = `服务正常 | 模型: ${d.llm_model} | 运行中任务: ${d.concurrent}`;
$('#health').classList.add('ok');
} catch (e) {
$('#health').textContent = '服务异常';
$('#health').classList.remove('ok');
}
}
const STATUS_MAP = {
queued: '排队中', running: '测试中', finished: '已完成', stopped: '已停止'
};
const RESULT_MAP = {
pending: '待定', pass: '✅ 通过', fail: '❌ 失败', error: '⚠️ 错误', stopped: '⏹️ 停止'
};
function esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => (
{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
async function loadTasks() {
try {
const r = await fetch(API + '/api/tasks');
const d = await r.json();
const tb = $('#task-table tbody');
if (!d.tasks.length) {
tb.innerHTML = '<tr><td colspan="8" class="empty">暂无任务,先发起一个吧</td></tr>';
return;
}
tb.innerHTML = d.tasks.map(t => {
const ops = t.status === 'running' || t.status === 'queued'
? `<button class="btn small" onclick="stopTask('${t.id}')">停止</button> `
: '';
const report = t.status === 'finished'
? `<a class="report-link" href="#" onclick="openReport('${t.id}');return false;">报告</a>`
: '';
return `<tr>
<td>${esc(t.id)}</td>
<td>${esc(t.url)}</td>
<td><div class="goal-cell" title="${esc(t.goal)}">${esc(t.goal)}</div></td>
<td><span class="status ${esc(t.status)}">${STATUS_MAP[t.status] || esc(t.status)}</span></td>
<td><span class="result ${esc(t.result)}">${RESULT_MAP[t.result] || esc(t.result)}</span></td>
<td>${t.steps || 0}</td>
<td>${esc(t.created)}</td>
<td><button class="btn small" onclick="openDetail('${t.id}')">详情</button> ${ops}${report}</td>
</tr>`;
}).join('');
} catch (e) {
$('#task-table tbody').innerHTML = '<tr><td colspan="8" class="empty">加载失败</td></tr>';
}
}
async function stopTask(tid) {
if (!confirm('确定停止该任务?')) return;
await fetch(API + `/api/tasks/${tid}/stop`, { method: 'POST' });
setTimeout(loadTasks, 500);
}
function openReport(tid) {
$('#report-frame').src = API + `/api/tasks/${tid}/report`;
$('#modal-title').textContent = `测试报告 - ${tid}`;
$('#report-modal').classList.remove('hidden');
}
/* ========== 任务详情 ========== */
let detailTid = null;
let detailTimer = null;
function openDetail(tid) {
detailTid = tid;
$('#detail-title').textContent = `任务详情 - ${tid}`;
$('#detail-steps').innerHTML = '<div class="empty">加载中...</div>';
$('#detail-modal').classList.remove('hidden');
refreshDetail();
detailTimer = setInterval(refreshDetail, 2500);
}
function closeDetail() {
detailTid = null;
if (detailTimer) { clearInterval(detailTimer); detailTimer = null; }
$('#detail-modal').classList.add('hidden');
}
const ACTION_LABEL = { click: '点击', fill: '填表', select: '选择', press: '按键',
wait: '等待', scroll: '滚动', assert: '断言', screenshot: '截图', done: '完成', fail: '失败' };
async function refreshDetail() {
if (!detailTid) return;
let t;
try {
const r = await fetch(API + `/api/tasks/${detailTid}`);
t = await r.json();
} catch (e) { return; }
// 元信息
const done = t.status === 'finished' || t.status === 'stopped';
$('#detail-meta').innerHTML = `
<div class="m-item"><b>目标网址</b><span>${esc(t.url)}</span></div>
<div class="m-item"><b>测试目标</b><span>${esc(t.goal)}</span></div>
<div class="m-item"><b>状态</b><span><span class="status ${esc(t.status)}">${STATUS_MAP[t.status] || esc(t.status)}</span> / <span class="result ${esc(t.result)}">${RESULT_MAP[t.result] || esc(t.result)}</span></span></div>
<div class="m-item"><b>步骤</b><span>${t.steps || 0} / ${t.max_steps}</span></div>
<div class="m-item"><b>创建时间</b><span>${esc(t.created)}</span></div>
${t.finished ? `<div class="m-item"><b>结束时间</b><span>${esc(t.finished)}</span></div>` : ''}
`;
// 实时状态条
const cur = t.current;
const liveBar = $('#live-bar');
if (!done && cur) {
liveBar.classList.remove('hidden');
$('#live-phase').textContent = `步骤 ${cur.step || '-'}: ${cur.phase || ''}` +
(cur.elapsed ? `(已运行 ${cur.elapsed}s` : '');
let d = cur.detail || '';
if (cur.reason) d += `<div class="live-reason">💡 ${esc(cur.reason)}</div>`;
$('#live-detail').innerHTML = esc(d);
} else {
liveBar.classList.add('hidden');
}
// 按钮
$('#detail-report-btn').classList.toggle('hidden', !done || t.result === 'error');
$('#detail-stop-btn').classList.toggle('hidden', done);
// 步骤列表
const steps = t.steps_log || [];
if (!steps.length) {
$('#detail-steps').innerHTML = '<div class="empty">还没有步骤记录,AI 正在准备...</div>';
} else {
$('#detail-steps').innerHTML = steps.map(s => {
const act = s.action || '';
const actCls = ['assert','click','fill','fail'].includes(act) ? act : '';
const shot = s.screenshot
? `<div class="step-shot"><img src="${API}/api/tasks/${detailTid}/screenshot/${esc(s.screenshot)}" loading="lazy" onclick="window.open(this.src)"></div>`
: '';
const resCls = s.result === 'ok' ? 'ok' : (s.result === 'fail' || s.result === 'error' ? 'bad' : '');
return `<div class="step-card">
<div class="step-top">
<span class="step-num">#${s.n}</span>
<span class="step-action ${actCls}">${ACTION_LABEL[act] || act}</span>
${s.target ? `<span class="step-target">${esc(s.target)}${s.value ? ' ' + esc(s.value) : ''}</span>` : ''}
<span class="step-result ${resCls}">${esc(s.result)}</span>
<span class="step-time">${esc(s.ts || '')}</span>
</div>
${s.reason ? `<div class="step-reason">💡 ${esc(s.reason)}</div>` : ''}
${s.detail ? `<div class="step-detail">${esc(s.detail)}</div>` : ''}
${shot}
</div>`;
}).join('');
}
// 结束后停止轮询
if (done) { if (detailTimer) { clearInterval(detailTimer); detailTimer = null; } }
}
$('#detail-close').onclick = closeDetail;
$('#detail-modal').onclick = e => { if (e.target === $('#detail-modal')) closeDetail(); };
$('#detail-stop-btn').onclick = async () => {
if (!detailTid || !confirm('确定停止该任务?')) return;
await fetch(API + `/api/tasks/${detailTid}/stop`, { method: 'POST' });
refreshDetail(); loadTasks();
};
$('#detail-report-btn').onclick = () => { if (detailTid) openReport(detailTid); };
$('#modal-close').onclick = () => $('#report-modal').classList.add('hidden');
$('#report-modal').onclick = e => { if (e.target === $('#report-modal')) $('#report-modal').classList.add('hidden'); };
$('#submit').onclick = async () => {
const url = $('#url').value.trim();
const goal = $('#goal').value.trim();
if (!url || !goal) { alert('请填写网址和测试目标'); return; }
const btn = $('#submit');
btn.disabled = true; btn.textContent = '提交中...';
try {
const r = await fetch(API + '/api/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url, goal,
max_steps: parseInt($('#max_steps').value) || 30,
timeout: parseInt($('#timeout').value) || 600
})
});
const d = await r.json();
if (d.error) { alert('提交失败: ' + d.error); }
else {
$('#goal').value = '';
loadTasks();
// 轮询直到该任务结束
pollTask(d.task_id);
}
} catch (e) { alert('提交失败: ' + e); }
finally { btn.disabled = false; btn.textContent = '🚀 开始测试'; }
};
function pollTask(tid, count = 0) {
if (count > 400) return;
setTimeout(async () => {
try {
const r = await fetch(API + `/api/tasks/${tid}`);
const t = await r.json();
loadTasks();
if (t.status === 'finished' || t.status === 'stopped') return;
pollTask(tid, count + 1);
} catch (e) { /* ignore */ }
}, 3000);
}
$('#refresh').onclick = loadTasks;
refreshHealth();
loadTasks();
setInterval(refreshHealth, 30000);
setInterval(loadTasks, 5000);