async function apiJson(url, options = undefined) { const resp = await fetch(url, options); if (!resp.ok) { const data = await resp.text(); throw new Error(data || `HTTP ${resp.status}`); } return resp.json(); } function esc(s) { return String(s || "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", })[c]); } async function refreshBilling() { const summary = await apiJson("/api/billing/me"); const rows = await apiJson("/api/billing/me/records?limit=20"); document.getElementById("billingSummary").textContent = `总 tokens=${summary.total_tokens} | 总费用(USD)=${Number( summary.total_cost_usd, ).toFixed(6)}(仅统计计费模型,不含 SiliconFlowFree 等免费模型)`; const body = document.getElementById("billingBody"); body.innerHTML = ""; for (const r of rows.records) { const cost = Number(r.cost_usd).toFixed(6); const costLabel = cost === "0.000000" ? `${cost}(不计费模型)` : cost; const tr = document.createElement("tr"); tr.innerHTML = ` ${esc(r.created_at)} ${esc(r.model)} ${r.prompt_tokens} ${r.completion_tokens} ${r.total_tokens} ${costLabel} `; body.appendChild(tr); } } // 任务状态缓存:在前端维护一个简单的内存表,方便 SSE/轮询统一渲染 const jobsState = new Map(); function actionButtons(job) { const actions = []; if (job.status === "queued" || job.status === "running") { actions.push( ``, ); } if (job.artifact_urls?.mono) { actions.push( ``, ); } if (job.artifact_urls?.dual) { actions.push( ``, ); } if (job.artifact_urls?.glossary) { actions.push( ``, ); } return actions.join(" "); } function statusText(status) { const statusMap = { queued: "排队中", running: "进行中", succeeded: "成功", failed: "失败", cancelled: "已取消", }; return statusMap[status] || status; } function renderJobsFromState() { const body = document.getElementById("jobsBody"); body.innerHTML = ""; const jobs = Array.from(jobsState.values()); jobs.sort((a, b) => (b.created_at || "").localeCompare(a.created_at || ""), ); for (const job of jobs) { const tr = document.createElement("tr"); tr.innerHTML = ` ${esc(job.id)} ${esc(job.filename)} ${esc(statusText(job.status))}${job.error ? " / " + esc(job.error) : ""} ${Number(job.progress).toFixed(1)}% ${esc(job.model)} ${esc(job.updated_at)} ${actionButtons(job)} `; body.appendChild(tr); } } function upsertJob(jobPatch) { const existing = jobsState.get(jobPatch.id) || {}; jobsState.set(jobPatch.id, { ...existing, ...jobPatch }); renderJobsFromState(); } async function refreshJobs() { const data = await apiJson("/api/jobs?limit=50"); jobsState.clear(); for (const job of data.jobs) { jobsState.set(job.id, job); } renderJobsFromState(); } async function cancelJob(jobId) { try { await apiJson(`/api/jobs/${jobId}/cancel`, { method: "POST" }); await refreshJobs(); } catch (err) { alert(`取消失败: ${err.message}`); } } document.getElementById("jobForm").addEventListener("submit", async (event) => { event.preventDefault(); const status = document.getElementById("jobStatus"); status.textContent = "提交中..."; const formData = new FormData(event.target); try { const created = await apiJson("/api/jobs", { method: "POST", body: formData }); status.textContent = `任务已入队: ${created.job.id}`; event.target.reset(); await refreshJobs(); } catch (err) { status.textContent = `提交失败: ${err.message}`; } }); async function refreshAll() { await Promise.all([refreshJobs(), refreshBilling()]); } let jobEventSource = null; let pollingEnabled = true; const POLL_INTERVAL_MS = 10000; function setupJobStream() { if (!("EventSource" in window)) { console.warn("EventSource not supported, fallback to polling"); pollingEnabled = true; return; } jobEventSource = new EventSource("/api/jobs/stream"); jobEventSource.onmessage = (event) => { try { const payload = JSON.parse(event.data); if (!payload || !payload.id) { return; } upsertJob(payload); } catch (err) { console.error("Failed to parse job SSE payload:", err); } }; jobEventSource.onerror = () => { console.error("Job SSE error, switching back to polling"); if (jobEventSource) { jobEventSource.close(); jobEventSource = null; } pollingEnabled = true; }; pollingEnabled = false; } refreshAll(); setupJobStream(); setInterval(async () => { if (document.hidden) { // 页面不可见时降低刷新频率:完全跳过本轮 return; } if (pollingEnabled) { await refreshAll(); } else { await refreshBilling(); } }, POLL_INTERVAL_MS);