feat: playbook执行日志实时流式推送(SSE) + 任务详情显示原始日志

This commit is contained in:
Hermes Agent
2026-05-18 15:08:48 +08:00
parent 6a7f74aac5
commit d58e860059
7 changed files with 197 additions and 44 deletions
+58 -9
View File
@@ -1246,6 +1246,11 @@
function closeModal(id) {
document.getElementById(id).classList.remove('active');
// 关闭任务日志 SSE 连接
if (id === 'taskDetailModal' && _taskLogSSE) {
_taskLogSSE.close();
_taskLogSSE = null;
}
}
// 编辑主机
@@ -1543,28 +1548,72 @@
}
}
// SSE 流式日志连接管理
let _taskLogSSE = null;
let _taskLogOutput = '';
async function viewTaskOutput(id) {
const res = await api(`/tasks/${id}`);
if (res.code === 0 && res.data) {
const task = res.data;
const content = document.getElementById('taskDetailContent');
// 关闭之前的 SSE 连接
if (_taskLogSSE) {
_taskLogSSE.close();
_taskLogSSE = null;
}
_taskLogOutput = task.output || '';
const isRunning = task.status === 'running';
content.innerHTML = `
<div style="margin-bottom: 15px;">
<strong>任务名称:</strong> ${task.name}<br>
<strong>状态:</strong> <span class="status ${task.status}">${statusText(task.status)}</span><br>
<strong>状态:</strong> <span class="status ${task.status}">${statusText(task.status)}</span>
${isRunning ? '<span class="spinner" style="margin-left:8px;"></span>' : ''}<br>
<strong>开始时间:</strong> ${task.start_time ? new Date(task.start_time).toLocaleString() : '-'}<br>
<strong>结束时间:</strong> ${task.end_time ? new Date(task.end_time).toLocaleString() : '-'}<br>
${task.error ? `<strong style="color:#ff6b6b;">错误:</strong> <span style="color:#ff6b6b;">${escapeHtml(task.error)}</span><br>` : ''}
</div>
<h3 style="color:#00d4aa;margin-bottom:10px;">执行结果</h3>
<div class="terminal">
${(task.results || []).map(r => `
<div class="line ${r.success ? 'success' : 'error'}">[${r.host}] ${r.success ? '✓' : '✗'} (${r.duration || 0}ms)</div>
${r.output ? `<div class="line">${escapeHtml(r.output)}</div>` : ''}
${r.error ? `<div class="line error">${escapeHtml(r.error)}</div>` : ''}
`).join('') || '<div class="line info">暂无结果</div>'}
</div>
<h3 style="color:#00d4aa;margin-bottom:10px;">执行日志 ${isRunning ? '<span style="color:#ffd93d;font-size:12px;">(实时更新中...)</span>' : ''}</h3>
<div class="terminal" id="taskLogTerminal" style="max-height:500px;overflow-y:auto;font-size:12px;line-height:1.6;white-space:pre-wrap;word-break:break-all;background:#0d1117;padding:15px;border-radius:8px;">${escapeHtml(_taskLogOutput) || '<span style="color:#8899a6;">等待输出...</span>'}</div>
`;
document.getElementById('taskDetailModal').classList.add('active');
// 运行中的任务用 SSE 实时推送
if (isRunning) {
const sseUrl = `${API_BASE}/tasks/${id}/stream`;
_taskLogSSE = new EventSource(sseUrl);
_taskLogSSE.addEventListener('log', function(e) {
_taskLogOutput += e.data;
const terminal = document.getElementById('taskLogTerminal');
if (terminal) {
terminal.textContent = _taskLogOutput;
terminal.scrollTop = terminal.scrollHeight;
}
});
_taskLogSSE.addEventListener('status', function(e) {
const statusEl = content.querySelector('.status');
if (statusEl) {
statusEl.className = `status ${e.data}`;
statusEl.textContent = statusText(e.data);
}
// 移除 spinner 和更新提示
const spinner = content.querySelector('.spinner');
if (spinner) spinner.remove();
const hint = content.querySelector('h3 span');
if (hint) hint.remove();
});
_taskLogSSE.addEventListener('error', function(e) {
// SSE 连接关闭(正常完成或断开)
if (_taskLogSSE) {
_taskLogSSE.close();
_taskLogSSE = null;
}
});
}
} else {
showToast('获取任务详情失败', 'error');
}