/* webtest-agent 前端逻辑 */ const $ = s => document.querySelector(s); const API = ''; async function refreshHealth() { try { const r = await fetch(API + '/health'); const d = await r.json(); const mode = d.llm_vision ? '🖼️视觉' : '📄文本'; $('#health').textContent = `服务正常 | 默认模型: ${d.llm_name || d.llm_model || '-'}(${mode}) | 运行中任务: ${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: '⏹️ 停止' }; /* ========== 大模型配置 ========== */ let llmConfigs = []; let editingLlmId = null; function llmBadge(cfg) { return cfg.vision ? '🖼️ 视觉' : '📄 文本'; } async function loadLlmConfigs() { try { const r = await fetch(API + '/api/llm-configs'); const d = await r.json(); llmConfigs = d.configs || []; renderLlmTable(); renderLlmSelect(); } catch (e) { /* ignore */ } } function renderLlmTable() { const tb = $('#llm-table tbody'); if (!llmConfigs.length) { tb.innerHTML = '暂无配置,点右上角「新增配置」添加'; return; } tb.innerHTML = llmConfigs.map(c => ` ${esc(c.name)} ${esc(c.model)} ${esc(c.base_url)} ${llmBadge(c)} ${c.temperature} ${c.timeout}s ${c.is_default ? '⭐' : ''} ${c.is_default ? '' : ``} ${c.is_default ? '' : ``} `).join(''); } function renderLlmSelect() { const sel = $('#llm_config_id'); if (!sel) return; if (!llmConfigs.length) { sel.innerHTML = ''; $('#model-tag').textContent = ''; return; } sel.innerHTML = llmConfigs.map(c => `` ).join(''); updateModelTag(); } function updateModelTag() { const id = parseInt($('#llm_config_id').value); const cfg = llmConfigs.find(c => c.id === id); $('#model-tag').textContent = cfg ? llmBadge(cfg) : ''; $('#model-tag').className = 'tag ' + (cfg && cfg.vision ? 'tag-vision' : 'tag-text'); } function openLlmModal(id) { editingLlmId = id || null; const c = id ? llmConfigs.find(x => x.id === id) : null; $('#llm-modal-title').textContent = c ? `编辑配置 - ${c.name}` : '新增模型配置'; $('#llm-name').value = c ? c.name : ''; $('#llm-base-url').value = c ? c.base_url : ''; $('#llm-api-key').value = c ? c.api_key : ''; $('#llm-model').value = c ? c.model : ''; $('#llm-vision').checked = c ? !!c.vision : false; $('#llm-default').checked = c ? !!c.is_default : false; $('#llm-temperature').value = c ? c.temperature : 0.2; $('#llm-timeout').value = c ? c.timeout : 120; $('#llm-test-result').textContent = ''; $('#llm-modal').classList.remove('hidden'); } function closeLlmModal() { $('#llm-modal').classList.add('hidden'); editingLlmId = null; } async function saveLlm() { const body = { name: $('#llm-name').value.trim(), base_url: $('#llm-base-url').value.trim(), api_key: $('#llm-api-key').value.trim(), model: $('#llm-model').value.trim(), vision: $('#llm-vision').checked, is_default: $('#llm-default').checked, temperature: parseFloat($('#llm-temperature').value) || 0.2, timeout: parseInt($('#llm-timeout').value) || 120 }; if (!body.name || !body.base_url || !body.api_key || !body.model) { alert('请填写名称、Base URL、API Key、模型名'); return; } const url = editingLlmId ? `${API}/api/llm-configs/${editingLlmId}` : `${API}/api/llm-configs`; const method = editingLlmId ? 'PUT' : 'POST'; try { const r = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const d = await r.json(); if (d.error) { alert('保存失败: ' + d.error); return; } closeLlmModal(); loadLlmConfigs(); } catch (e) { alert('保存失败: ' + e); } } async function deleteLlm(id) { const c = llmConfigs.find(x => x.id === id); if (!confirm(`确定删除配置「${c.name}」?`)) return; const r = await fetch(API + `/api/llm-configs/${id}`, { method: 'DELETE' }); const d = await r.json(); if (d.error) { alert('删除失败: ' + d.error); return; } loadLlmConfigs(); } async function setDefaultLlm(id) { await fetch(API + `/api/llm-configs/${id}/default`, { method: 'POST' }); loadLlmConfigs(); } async function testLlm(id) { const c = llmConfigs.find(x => x.id === id); const el = $('#llm-test-result'); if (el) el.textContent = ''; try { const r = await fetch(API + `/api/llm-configs/${id}/test`, { method: 'POST' }); const d = await r.json(); if (d.ok) { alert(`✅ 连接成功!模型回复: ${d.reply}`); } else { alert(`❌ 连接失败: ${d.error}`); } } catch (e) { alert('测试请求失败: ' + e); } } $('#add-llm').onclick = () => openLlmModal(null); $('#llm-modal-close').onclick = closeLlmModal; $('#llm-cancel').onclick = closeLlmModal; $('#llm-save').onclick = saveLlm; $('#llm-modal').onclick = e => { if (e.target === $('#llm-modal')) closeLlmModal(); }; $('#llm-test').onclick = async () => { // 用表单当前内容测试(未保存也能测) const body = { name: $('#llm-name').value.trim() || '测试', base_url: $('#llm-base-url').value.trim(), api_key: $('#llm-api-key').value.trim(), model: $('#llm-model').value.trim(), vision: $('#llm-vision').checked, temperature: parseFloat($('#llm-temperature').value) || 0.2, timeout: parseInt($('#llm-timeout').value) || 120 }; if (!body.base_url || !body.api_key || !body.model) { alert('请先填写 Base URL、API Key、模型名再测试'); return; } const el = $('#llm-test-result'); el.textContent = '测试中...'; try { const r = await fetch(API + '/api/llm-configs/test-form', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const d = await r.json(); el.textContent = d.ok ? `✅ ${d.reply}` : `❌ ${d.error}`; } catch (e) { el.textContent = '❌ ' + e; } }; $('#llm_config_id').onchange = updateModelTag; 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)}
${t.llm_name ? `${esc(t.llm_name)} ${t.vision ? '🖼️' : '📄'}` : '-'} ${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)}
分析模型${t.llm_name ? esc(t.llm_name) + ' ' + (t.vision ? '🖼️' : '📄') : '-'}
状态${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, llm_config_id: parseInt($('#llm_config_id').value) || null }) }); 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(); loadLlmConfigs(); setInterval(refreshHealth, 30000); setInterval(loadTasks, 5000);