(() => { "use strict"; if (window.self !== window.top || new URLSearchParams(window.location.search).has("embedded")) { document.body.classList.add("embedded"); } const STATUS_LABELS = { waiting: "等待", running: "运行中", pending_confirmation: "待确认", success: "成功", failed: "失败", canceled: "取消", blocked: "阻塞", published: "已发布", ready: "待发布", baseline: "基线固件", }; const STAGE_ICONS = { instruction: "file-input", requirements: "list-checks", baseline: "git-compare-arrows", codegen: "code-2", build: "hammer", test: "shield-check", package: "package-check", publish: "cloud-upload", }; const ACTIVE_STATES = new Set(["waiting", "running", "pending_confirmation"]); const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热", "动态木马"]; const FALLBACK_BASELINES = { time_only: ["时间控制"], }; const FALLBACK_PIPELINE = [ ["instruction", "指令输入"], ["requirements", "需求解析"], ["baseline", "基线检查"], ["codegen", "代码生成"], ["build", "编译构建"], ["test", "测试验证"], ["package", "固件打包"], ["publish", "发布服务器"], ].map(([id, name]) => ({ id, name, status: "waiting", summary: "等待执行" })); const ui = { instruction: document.querySelector("#instructionInput"), baseline: document.querySelector("#baselineSelect"), baselineCapabilities: document.querySelector("#baselineCapabilities"), targetCapabilities: document.querySelector("#targetCapabilities"), start: document.querySelector("#startTaskButton"), cancel: document.querySelector("#cancelTaskButton"), retry: document.querySelector("#retryTaskButton"), server: document.querySelector("#serverState"), serverName: document.querySelector("#otaServerName"), release: document.querySelector("#releaseResult"), pipeline: document.querySelector("#pipeline"), logs: document.querySelector("#logStream"), scrollLogs: document.querySelector("#scrollLogButton"), status: document.querySelector("#taskStatusBadge"), metrics: document.querySelector("#taskMetrics"), inspector: document.querySelector("#inspectorContent"), artifacts: document.querySelector("#artifactTableBody"), modal: document.querySelector("#detailModal"), modalTitle: document.querySelector("#modalTitle"), modalBody: document.querySelector("#modalBody"), modalClose: document.querySelector("#closeModalButton"), modalCloseFooter: document.querySelector("#modalCloseButton"), toasts: document.querySelector("#toastRegion"), }; const model = { snapshot: { tasks: [], artifacts: [], releases: [], pipeline: FALLBACK_PIPELINE }, selectedTaskId: null, inspectorTab: "changes", pending: false, pollTimer: null, lastLogCount: 0, }; function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } async function api(path, options = {}) { const response = await fetch(path, { credentials: "same-origin", headers: { "Content-Type": "application/json", ...(options.headers || {}) }, ...options, }); const contentType = response.headers.get("content-type") || ""; const payload = contentType.includes("application/json") ? await response.json() : null; if (!response.ok) throw new Error(payload?.message || `请求失败(${response.status})`); return payload; } function selectedTask() { return model.snapshot.tasks.find((task) => task.id === model.selectedTaskId) || model.snapshot.tasks[0] || null; } function baselineCapabilities() { const baseline = model.snapshot.baselines?.find((item) => item.id === ui.baseline.value); return baseline?.capabilities || FALLBACK_BASELINES[ui.baseline.value] || FALLBACK_BASELINES.time_only; } function parseInstructionCapabilities(instruction) { const lowered = String(instruction || "").toLowerCase(); const aliases = { "时间控制": ["时间", "定时", "timed"], "位置控制": ["位置", "position"], "座椅通风": ["通风", "vent"], "座椅加热": ["加热", "heater", "heat"], "动态木马": ["木马", "rocking_horse", "rocking horse", "rocking-horse"], }; const requested = CAPABILITY_ORDER.filter((capability) => aliases[capability].some((alias) => lowered.includes(alias))); if (requested.includes("动态木马") && !requested.includes("位置控制")) requested.push("位置控制"); return CAPABILITY_ORDER.filter((capability) => requested.includes(capability)); } function updateCapabilityPreview(task = null) { const baseline = baselineCapabilities(); const taskMatchesDraft = task && task.instruction === ui.instruction.value.trim() && (task.baseline_id || "time_only") === ui.baseline.value; const target = taskMatchesDraft ? task.target_capabilities : CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item)); ui.baselineCapabilities.textContent = baseline.join("+"); ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+"); ui.targetCapabilities.title = ui.targetCapabilities.textContent; } function formatDate(value) { if (!value) return "—"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }).format(date).replaceAll("/", "-"); } function formatClock(value) { if (!value) return "--:--:--"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "--:--:--"; return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }).format(date); } function formatDuration(milliseconds) { if (milliseconds === null || milliseconds === undefined) return "—"; if (milliseconds < 1000) return `${milliseconds} 毫秒`; const seconds = milliseconds / 1000; return seconds < 60 ? `${seconds.toFixed(seconds < 10 ? 1 : 0)} 秒` : `${Math.floor(seconds / 60)} 分 ${Math.round(seconds % 60)} 秒`; } function formatSize(bytes) { const value = Number(bytes || 0); if (value < 1024) return `${value} 字节`; if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} 千字节`; return `${(value / 1024 / 1024).toFixed(2)} 兆字节`; } function toast(message, type = "info") { const element = document.createElement("div"); element.className = `toast ${type}`; element.textContent = message; ui.toasts.append(element); window.setTimeout(() => element.remove(), 4200); } function refreshIcons() { if (window.lucide) window.lucide.createIcons({ attrs: { "stroke-width": 1.8 } }); } function renderPipeline(task) { const stages = task?.stages || model.snapshot.pipeline?.map((stage) => ({ ...stage, status: "waiting" })) || FALLBACK_PIPELINE; ui.pipeline.innerHTML = stages.map((stage) => `
  • ${escapeHtml(stage.name)} ${escapeHtml(STATUS_LABELS[stage.status] || stage.status || "等待")}${stage.duration_ms !== null && stage.duration_ms !== undefined ? ` · ${escapeHtml(formatDuration(stage.duration_ms))}` : ""}
  • `).join(""); } function renderRelease(task) { let icon = "cloud-upload"; let label = "尚未开始"; let state = "idle"; if (task) { label = task.message || STATUS_LABELS[task.status] || task.status; state = task.status; if (task.status === "success") { icon = "circle-check-big"; state = "success"; } else if (task.status === "running") icon = "loader-circle"; else if (task.status === "failed") icon = "circle-x"; else if (task.status === "canceled") icon = "ban"; } ui.release.dataset.state = state; ui.release.innerHTML = `
    当前交付状态${escapeHtml(label)}
    `; } function renderLogs(task) { const logs = task?.logs || []; if (!logs.length) { ui.logs.innerHTML = `
    暂无执行记录创建开发任务后,这里将实时显示各阶段输出。
    `; model.lastLogCount = 0; return; } const nearBottom = ui.logs.scrollHeight - ui.logs.scrollTop - ui.logs.clientHeight < 80; ui.logs.innerHTML = logs.map((entry) => `
    ${escapeHtml(entry.title)}

    ${escapeHtml(entry.message)}

    `).join(""); if (nearBottom || logs.length > model.lastLogCount) ui.logs.scrollTop = ui.logs.scrollHeight; model.lastLogCount = logs.length; } function renderInspector(task) { const status = task?.status || "waiting"; ui.status.dataset.status = status; ui.status.textContent = STATUS_LABELS[status] || status; const fileCount = task?.file_changes?.length || 0; ui.metrics.innerHTML = `
    开始时间
    ${escapeHtml(formatDate(task?.started_at))}
    结束时间
    ${escapeHtml(formatDate(task?.finished_at))}
    实际耗时
    ${escapeHtml(formatDuration(task?.duration_ms))}
    文件变更
    ${fileCount ? `${fileCount} 个文件` : "—"}
    `; document.querySelectorAll("[data-inspector-tab]").forEach((button) => { button.classList.toggle("active", button.dataset.inspectorTab === model.inspectorTab); }); if (!task) { ui.inspector.innerHTML = `
    暂无任务数据
    `; return; } if (model.inspectorTab === "build") { ui.inspector.innerHTML = task.build_output?.length ? `
    ${escapeHtml(task.build_output.join("\n"))}
    ` : `
    尚无编译输出
    `; return; } if (model.inspectorTab === "tests") { ui.inspector.innerHTML = task.test_results?.length ? task.test_results.map((test) => `
    ${escapeHtml(test.name)}${escapeHtml(test.summary)}
    `).join("") : `
    测试尚未执行
    `; return; } ui.inspector.innerHTML = task.file_changes?.length ? task.file_changes.map((change) => `
    ${change.type === "added" ? "新增" : change.type === "retained" ? "复用" : "修改"}
    ${escapeHtml(change.path)}${escapeHtml(change.summary)}
    `).join("") : `
    尚无文件变更
    `; } function renderArtifacts() { const artifacts = model.snapshot.artifacts || []; if (!artifacts.length) { ui.artifacts.innerHTML = `
    尚未生成固件产物
    `; return; } ui.artifacts.innerHTML = artifacts.map((artifact) => `
    ${escapeHtml(artifact.software_name)}${escapeHtml(artifact.version)}
    ${(artifact.capabilities || []).map((capability) => `${escapeHtml(capability)}`).join("")}
    ${escapeHtml(formatDate(artifact.build_time))}
    ${escapeHtml(artifact.firmware_filename)}${escapeHtml(formatSize(artifact.size_bytes))} · ${escapeHtml(artifact.sha256)}
    ${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}
    ${artifact.is_baseline ? `受保护` : ``} ${artifact.status === "published" ? `` : ""}
    `).join(""); } function render() { const task = selectedTask(); if (task) model.selectedTaskId = task.id; renderPipeline(task); renderRelease(task); renderLogs(task); renderInspector(task); renderArtifacts(); updateCapabilityPreview(task); const active = task && ACTIVE_STATES.has(task.status); ui.start.disabled = model.pending || Boolean(active); ui.cancel.disabled = model.pending || !active; ui.retry.disabled = model.pending || !task || !["failed", "canceled", "blocked"].includes(task.status); ui.start.innerHTML = model.pending ? `正在提交` : `开始生成`; refreshIcons(); } async function refreshState({ quiet = false } = {}) { try { model.snapshot = await api("/api/firmware-workbench/state"); if (!model.selectedTaskId && model.snapshot.tasks?.length) model.selectedTaskId = model.snapshot.tasks[0].id; ui.server.classList.add("online"); ui.server.innerHTML = `发布服务已连接`; ui.serverName.textContent = model.snapshot.ota_server?.name || "车机升级服务器"; render(); } catch (error) { ui.server.classList.remove("online"); ui.server.innerHTML = `发布服务连接失败`; if (!quiet) toast(error.message, "error"); } finally { schedulePoll(); } } function schedulePoll() { window.clearTimeout(model.pollTimer); const active = model.snapshot.tasks?.some((task) => ACTIVE_STATES.has(task.status)); model.pollTimer = window.setTimeout(() => refreshState({ quiet: true }), active ? 700 : 5000); } async function startTask() { const instruction = ui.instruction.value.trim(); if (!instruction) { toast("请输入开发指令。", "error"); ui.instruction.focus(); return; } model.pending = true; render(); try { const result = await api("/api/firmware-workbench/tasks", { method: "POST", body: JSON.stringify({ instruction, baseline_id: ui.baseline.value }), }); model.selectedTaskId = result.task.id; toast("开发任务已创建,流水线开始执行。", "success"); await refreshState({ quiet: true }); } catch (error) { toast(error.message, "error"); } finally { model.pending = false; render(); } } async function taskAction(action) { const task = selectedTask(); if (!task) return; model.pending = true; render(); try { await api(`/api/firmware-workbench/tasks/${encodeURIComponent(task.id)}/${action}`, { method: "POST", body: "{}" }); toast(action === "cancel" ? "取消请求已提交。" : "任务已重新进入流水线。", "success"); await refreshState({ quiet: true }); } catch (error) { toast(error.message, "error"); } finally { model.pending = false; render(); } } function appendPrompt(text) { const current = ui.instruction.value.trim(); if (current.includes(text)) return; ui.instruction.value = current ? `${current.replace(/[。;;]?$/, "")};${text}。` : `${text}。`; ui.instruction.focus(); updateCapabilityPreview(); } function showModal(title, content) { ui.modalTitle.textContent = title; ui.modalBody.innerHTML = content; ui.modal.hidden = false; document.body.style.overflow = "hidden"; refreshIcons(); ui.modalClose.focus(); } function closeModal() { ui.modal.hidden = true; document.body.style.overflow = ""; } async function artifactAction(button) { const artifact = model.snapshot.artifacts.find((item) => item.id === button.dataset.artifactId); if (!artifact) return; const action = button.dataset.artifactAction; if (action === "delete") { if (!window.confirm(`确认删除“${artifact.firmware_filename}”及其服务器发布记录吗?此操作不可恢复。`)) return; button.disabled = true; try { const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}`, { method: "DELETE" }); toast(result.message, "success"); await refreshState({ quiet: true }); } catch (error) { toast(error.message, "error"); } finally { button.disabled = false; } return; } if (action === "publish") { button.disabled = true; try { const release = await api(`/api/firmware-workbench/artifacts/${artifact.id}/publish`, { method: "POST", body: "{}" }); toast(`${release.message}(${release.id})`, "success"); await refreshState({ quiet: true }); } catch (error) { toast(error.message, "error"); } finally { button.disabled = false; } return; } if (action === "notify") { button.disabled = true; try { const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}/notify`, { method: "POST", body: "{}" }); toast(`已向车端发送 ${result.published_count || result.burst_count || 3} 帧通知。`, "success"); await refreshState({ quiet: true }); } catch (error) { toast(error.message, "error"); } finally { button.disabled = false; } return; } if (action === "detail") { showModal("固件产物详情", `
    软件名称${escapeHtml(artifact.software_name)}
    版本${escapeHtml(artifact.version)}
    固件包${escapeHtml(artifact.firmware_filename)}
    构建时间${escapeHtml(formatDate(artifact.build_time))}
    固件包大小${escapeHtml(formatSize(artifact.size_bytes))}
    发布状态${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}
    SHA-256(固件包)${escapeHtml(artifact.sha256)}
    SHA-256(HEX 镜像)${escapeHtml(artifact.image_sha256)}
    `); } else if (action === "capabilities") { const diff = artifact.capability_diff || {}; showModal("能力差异", `

    固件基线

    ${(artifact.baseline_capabilities || []).map((item) => `

    • ${escapeHtml(item)}

    `).join("")}

    目标固件变化

    ${(diff.added || []).map((item) => `

    + ${escapeHtml(item)}

    `).join("")}${(diff.retained || []).map((item) => `

    ✓ 保留${escapeHtml(item)}

    `).join("") || "

    无新增能力

    "}
    `); } else if (action === "code") { showModal("代码差异", `
    ${escapeHtml(artifact.code_diff || "暂无代码差异记录")}
    `); } else if (action === "validation") { showModal("验证记录", (artifact.validation_records || []).map((test) => `
    ${escapeHtml(test.name)}${escapeHtml(test.summary)} · ${escapeHtml(formatDate(test.finished_at))}
    `).join("") || `
    暂无验证记录
    `); } } ui.start.addEventListener("click", startTask); ui.instruction.addEventListener("input", () => updateCapabilityPreview()); ui.baseline.addEventListener("change", () => updateCapabilityPreview()); ui.cancel.addEventListener("click", () => taskAction("cancel")); ui.retry.addEventListener("click", () => taskAction("retry")); ui.scrollLogs.addEventListener("click", () => { ui.logs.scrollTop = ui.logs.scrollHeight; }); document.querySelectorAll("[data-prompt]").forEach((button) => button.addEventListener("click", () => appendPrompt(button.dataset.prompt))); document.querySelectorAll("[data-inspector-tab]").forEach((button) => button.addEventListener("click", () => { model.inspectorTab = button.dataset.inspectorTab; renderInspector(selectedTask()); refreshIcons(); })); ui.artifacts.addEventListener("click", (event) => { const button = event.target.closest("[data-artifact-action]"); if (button) artifactAction(button); }); ui.modalClose.addEventListener("click", closeModal); ui.modalCloseFooter.addEventListener("click", closeModal); ui.modal.addEventListener("click", (event) => { if (event.target === ui.modal) closeModal(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !ui.modal.hidden) closeModal(); }); renderPipeline(null); renderRelease(null); updateCapabilityPreview(); refreshIcons(); refreshState(); })();