feat: playbook执行日志实时流式推送(SSE) + 任务详情显示原始日志
This commit is contained in:
Vendored
+58
-9
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user