新增任务实时详情:Agent实时状态上报(决策/执行/验证阶段)+前端详情面板(2.5s轮询步骤日志/截图/停止按钮);修复static路径
This commit is contained in:
+102
-3
@@ -40,8 +40,8 @@ async function loadTasks() {
|
||||
? `<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>`
|
||||
: '—';
|
||||
? `<a class="report-link" href="#" onclick="openReport('${t.id}');return false;">报告</a>`
|
||||
: '';
|
||||
return `<tr>
|
||||
<td>${esc(t.id)}</td>
|
||||
<td>${esc(t.url)}</td>
|
||||
@@ -50,7 +50,7 @@ async function loadTasks() {
|
||||
<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>${ops}${report}</td>
|
||||
<td><button class="btn small" onclick="openDetail('${t.id}')">详情</button> ${ops}${report}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
@@ -70,6 +70,105 @@ function openReport(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'); };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user