| window.PluginUI = (() => { |
| async function request(path, options = {}) { |
| const response = await fetch(path, options); |
| const contentType = response.headers.get("content-type") || ""; |
| const payload = contentType.includes("application/json") |
| ? await response.json() |
| : await response.text(); |
|
|
| if (!response.ok) { |
| const detail = typeof payload === "object" ? payload.detail || payload.message : payload; |
| throw new Error(detail || `请求失败:${response.status}`); |
| } |
|
|
| return payload; |
| } |
|
|
| async function requestBlob(path, options = {}) { |
| const response = await fetch(path, options); |
| if (!response.ok) { |
| let detail = ""; |
| try { |
| const payload = await response.json(); |
| detail = payload.detail || payload.message || ""; |
| } catch { |
| detail = await response.text(); |
| } |
| throw new Error(detail || `请求失败:${response.status}`); |
| } |
| return response.blob(); |
| } |
|
|
| function showMessage(id, text, type = "success") { |
| const el = document.getElementById(id); |
| if (!el) return; |
| el.textContent = text; |
| el.className = `plugin-message show ${type}`; |
| } |
|
|
| function clearMessage(id) { |
| const el = document.getElementById(id); |
| if (!el) return; |
| el.textContent = ""; |
| el.className = "plugin-message"; |
| } |
|
|
| function setText(id, value) { |
| const el = document.getElementById(id); |
| if (el) el.textContent = value == null ? "" : String(value); |
| } |
|
|
| function setJson(id, value) { |
| setText(id, typeof value === "string" ? value : JSON.stringify(value, null, 2)); |
| } |
|
|
| function downloadBlob(blob, filename) { |
| const url = URL.createObjectURL(blob); |
| const link = document.createElement("a"); |
| link.href = url; |
| link.download = filename; |
| document.body.appendChild(link); |
| link.click(); |
| link.remove(); |
| URL.revokeObjectURL(url); |
| } |
|
|
| function fileSize(bytes) { |
| if (!Number.isFinite(bytes)) return "-"; |
| if (bytes < 1024) return `${bytes} B`; |
| if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; |
| return `${(bytes / 1024 / 1024).toFixed(1)} MB`; |
| } |
|
|
| async function copyText(text) { |
| await navigator.clipboard.writeText(text); |
| } |
|
|
| |
| |
| |
|
|
| |
| function _formatTs(ts) { |
| try { |
| const d = new Date(ts); |
| const pad = (n) => String(n).padStart(2, "0"); |
| return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; |
| } catch { |
| return ts || ""; |
| } |
| } |
|
|
| |
| function _levelClass(level) { |
| if (level === "error") return "run-log-entry--error"; |
| if (level === "warning") return "run-log-entry--warn"; |
| return ""; |
| } |
|
|
| |
| function _levelLabel(level) { |
| if (level === "error") return "错误"; |
| if (level === "warning") return "警告"; |
| if (level === "debug") return "调试"; |
| return "信息"; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function createRunLog(options = {}) { |
| const title = options.title || "运行日志"; |
|
|
| const panel = document.createElement("div"); |
| panel.className = "plugin-panel run-log-panel"; |
|
|
| const titleEl = document.createElement("h3"); |
| titleEl.className = "plugin-panel-title"; |
| titleEl.textContent = title; |
| panel.appendChild(titleEl); |
|
|
| |
| const statusBar = document.createElement("div"); |
| statusBar.className = "run-log-status"; |
| statusBar.innerHTML = `<span class="run-log-status-dot"></span><span class="run-log-status-text">等待运行...</span>`; |
| panel.appendChild(statusBar); |
|
|
| |
| const logList = document.createElement("div"); |
| logList.className = "run-log-list"; |
| panel.appendChild(logList); |
|
|
| |
| panel._runLogState = { |
| seenSeq: new Set(), |
| pollingTimer: null, |
| apiBase: "", |
| runId: "", |
| statusBar, |
| logList, |
| }; |
|
|
| return panel; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function _renderEvent(logPanel, event) { |
| const state = logPanel._runLogState; |
| if (!state) return; |
|
|
| |
| if (state.seenSeq.has(event.seq)) return; |
| state.seenSeq.add(event.seq); |
|
|
| const entry = document.createElement("div"); |
| entry.className = `run-log-entry ${_levelClass(event.level)}`; |
|
|
| |
| const header = document.createElement("div"); |
| header.className = "run-log-entry-header"; |
|
|
| const ts = document.createElement("span"); |
| ts.className = "run-log-entry-ts"; |
| ts.textContent = _formatTs(event.ts); |
|
|
| const stage = document.createElement("span"); |
| stage.className = "run-log-entry-stage"; |
| stage.textContent = event.stage || ""; |
|
|
| const level = document.createElement("span"); |
| level.className = `run-log-entry-level run-log-level--${event.level || "info"}`; |
| level.textContent = _levelLabel(event.level); |
|
|
| header.appendChild(ts); |
| header.appendChild(stage); |
| header.appendChild(level); |
| entry.appendChild(header); |
|
|
| |
| const msg = document.createElement("div"); |
| msg.className = "run-log-entry-message"; |
| msg.textContent = event.message || ""; |
| entry.appendChild(msg); |
|
|
| |
| if (event.detail) { |
| const detail = document.createElement("details"); |
| detail.className = "run-log-entry-detail"; |
| const summary = document.createElement("summary"); |
| summary.textContent = "详情"; |
| detail.appendChild(summary); |
| const pre = document.createElement("pre"); |
| pre.textContent = event.detail; |
| detail.appendChild(pre); |
| entry.appendChild(detail); |
| } |
|
|
| |
| if (event.artifact_id) { |
| const artifactLink = document.createElement("div"); |
| artifactLink.className = "run-log-entry-artifact"; |
| artifactLink.innerHTML = `<span class="run-log-artifact-icon">📎</span>产物: <code>${event.artifact_id}</code>`; |
| entry.appendChild(artifactLink); |
| } |
|
|
| state.logList.appendChild(entry); |
| |
| state.logList.scrollTop = state.logList.scrollHeight; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function pollRunEvents(pluginName, runId, logPanel, options = {}) { |
| const state = logPanel._runLogState; |
| if (!state) return; |
|
|
| |
| if (state.pollingTimer) { |
| clearInterval(state.pollingTimer); |
| state.pollingTimer = null; |
| } |
|
|
| state.pluginName = pluginName; |
| state.runId = runId; |
|
|
| const interval = options.interval || 1500; |
|
|
| |
| state.statusBar.querySelector(".run-log-status-text").textContent = "运行中..."; |
| state.statusBar.querySelector(".run-log-status-dot").className = |
| "run-log-status-dot run-log-status-dot--running"; |
|
|
| |
| const runBase = `/api/plugins/${pluginName}/runs/${runId}`; |
|
|
| const poll = async () => { |
| try { |
| |
| const runResp = await request(runBase); |
| const runStatus = runResp.status; |
|
|
| |
| const eventsResp = await request(`${runBase}/events`); |
| const events = eventsResp.events || []; |
|
|
| for (const event of events) { |
| _renderEvent(logPanel, event); |
| } |
|
|
| |
| if (runStatus === "succeeded" || runStatus === "failed") { |
| clearInterval(state.pollingTimer); |
| state.pollingTimer = null; |
|
|
| const dot = state.statusBar.querySelector(".run-log-status-dot"); |
| const text = state.statusBar.querySelector(".run-log-status-text"); |
|
|
| if (runStatus === "succeeded") { |
| dot.className = "run-log-status-dot run-log-status-dot--done"; |
| text.textContent = "运行完成"; |
| } else { |
| dot.className = "run-log-status-dot run-log-status-dot--error"; |
| text.textContent = runResp.error |
| ? `运行失败: ${runResp.error}` |
| : "运行失败"; |
| } |
|
|
| if (options.onRunComplete) { |
| options.onRunComplete(runResp); |
| } |
| } |
| } catch (err) { |
| |
| state.statusBar.querySelector(".run-log-status-dot").className = |
| "run-log-status-dot run-log-status-dot--error"; |
| state.statusBar.querySelector(".run-log-status-text").textContent = |
| `轮询错误: ${err.message}`; |
|
|
| if (options.onError) { |
| options.onError(err); |
| } |
| } |
| }; |
|
|
| |
| poll(); |
| |
| state.pollingTimer = setInterval(poll, interval); |
| } |
|
|
| |
| |
| |
| |
| |
| function stopPolling(logPanel) { |
| const state = logPanel._runLogState; |
| if (state && state.pollingTimer) { |
| clearInterval(state.pollingTimer); |
| state.pollingTimer = null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function renderArtifactLink(artifact, container, pluginName, runId) { |
| const item = document.createElement("div"); |
| item.className = "plugin-item"; |
|
|
| const info = document.createElement("div"); |
| info.innerHTML = ` |
| <strong>${escapeHtml(artifact.filename)}</strong> |
| <div class="plugin-muted" style="font-size:12px">${fileSize(artifact.size)} · ${escapeHtml(artifact.media_type)}</div> |
| `; |
| item.appendChild(info); |
|
|
| const btn = document.createElement("button"); |
| btn.className = "plugin-button secondary"; |
| btn.textContent = "下载"; |
| btn.addEventListener("click", async () => { |
| try { |
| const blob = await requestBlob( |
| `/api/plugins/${pluginName}/runs/${runId}/artifacts/${artifact.artifact_id}/download` |
| ); |
| downloadBlob(blob, artifact.filename); |
| } catch (err) { |
| alert(`下载失败: ${err.message}`); |
| } |
| }); |
| item.appendChild(btn); |
|
|
| container.appendChild(item); |
| return item; |
| } |
|
|
| |
| function escapeHtml(str) { |
| if (!str) return ""; |
| const el = document.createElement("span"); |
| el.textContent = str; |
| return el.innerHTML; |
| } |
|
|
| return { |
| request, |
| requestBlob, |
| showMessage, |
| clearMessage, |
| setText, |
| setJson, |
| downloadBlob, |
| fileSize, |
| copyText, |
| createRunLog, |
| pollRunEvents, |
| stopPolling, |
| renderArtifactLink, |
| escapeHtml, |
| }; |
| })(); |
|
|