/* 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 => ( {'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 = '暂无任务,先发起一个吧'; return; } tb.innerHTML = d.tasks.map(t => { const ops = t.status === 'running' || t.status === 'queued' ? ` ` : ''; const report = t.status === 'finished' ? `报告` : ''; return ` ${esc(t.id)} ${esc(t.url)}
${esc(t.goal)}
${STATUS_MAP[t.status] || esc(t.status)} ${RESULT_MAP[t.result] || esc(t.result)} ${t.steps || 0} ${esc(t.created)} ${ops}${report} `; }).join(''); } catch (e) { $('#task-table tbody').innerHTML = '加载失败'; } } 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 = '
加载中...
'; $('#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 = `
目标网址${esc(t.url)}
测试目标${esc(t.goal)}
状态${STATUS_MAP[t.status] || esc(t.status)} / ${RESULT_MAP[t.result] || esc(t.result)}
步骤${t.steps || 0} / ${t.max_steps}
创建时间${esc(t.created)}
${t.finished ? `
结束时间${esc(t.finished)}
` : ''} `; // 实时状态条 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 += `
💡 ${esc(cur.reason)}
`; $('#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 = '
还没有步骤记录,AI 正在准备...
'; } else { $('#detail-steps').innerHTML = steps.map(s => { const act = s.action || ''; const actCls = ['assert','click','fill','fail'].includes(act) ? act : ''; const shot = s.screenshot ? `
` : ''; const resCls = s.result === 'ok' ? 'ok' : (s.result === 'fail' || s.result === 'error' ? 'bad' : ''); return `
#${s.n} ${ACTION_LABEL[act] || act} ${s.target ? `${esc(s.target)}${s.value ? ' ' + esc(s.value) : ''}` : ''} ${esc(s.result)} ${esc(s.ts || '')}
${s.reason ? `
💡 ${esc(s.reason)}
` : ''} ${s.detail ? `
${esc(s.detail)}
` : ''} ${shot}
`; }).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);