// Stage OS is the single-window application shell over the existing native bridge.
// Business capabilities are routed inside MainWindow; only OverlayWindow may be visible separately.
const stageOsCoachAsset = "assets/stage-os/ai-coach-elf.png";
const stageOsCoachHeroAsset = "assets/stage-os/ai-coach-elf-hero.png";
window.__stageOsErrors = [];
window.addEventListener("error", (event) => {
  window.__stageOsErrors.push(String(event?.message || "unknown error"));
});
window.addEventListener("unhandledrejection", (event) => {
  window.__stageOsErrors.push(String(event?.reason || "unhandled rejection"));
});
const stageOsBaseReceive = window.LiveCoachNative.receive.bind(window.LiveCoachNative);
const stageOsBaseWirePageActions = wirePageActions;
let stageOsInitialStateHandled = false;
let stageOsSessionPhase = "prep";
let stageOsRemindersPaused = false;
let stageOsAvatarMenuOpen = false;
let stageOsEndConfirmOpen = false;
let stageOsSafeExit = null;
let stageOsPendingPadDeleteId = "";
let stageOsProcessingTimer = null;
let stageOsSessionStartedAt = null;
let stageOsAudioModeDraft = null;
let stageOsOverlayVisible = false;
let stageOsPreflightAudioOpen = false;
let stageOsLocalModeActive = false;
let stageOsSelectedPadId = "";
let stageOsAudioSearch = "";
let stageOsAudioFavoriteOnly = false;
let stageOsCurrentRoute = "prep";
let stageOsPostLiveSelectedIndex = 0;
let stageOsPostLiveSelectedOpener = 0;
let stageOsPostLiveLoading = false;
let stageOsAudioSettingsMessage = "";
let stageOsAccountMessage = "";
let stageOsRecoveryCode = "";
let stageOsRecoveryClearTimer = null;
let stageOsAccountLogoutConfirmOpen = false;
let stageOsAccountBusy = false;
let stageOsAccount = {
  configured: false,
  logged_in: false,
  status: "disabled",
  registration_mode: "activation",
  account_id: "",
  display_name: "",
  plan: "",
  expires_at_ms: 0,
  max_devices: 0,
  daily_request_limit: 0,
  daily_request_count: 0,
  daily_input_tokens: 0,
  daily_output_tokens: 0,
  device_name: "",
  managed_ai_ready: false,
  message: ""
};
let stageOsDemoState = {
  enabled: false,
  scenarios: [],
  state: {
    running: false,
    paused: false,
    scenario: "new-viewer",
    mode: "fixed",
    delay_seconds: 1.8,
    status: "演示控制器尚未启动",
    step_index: 0,
    step_count: 0,
    logs: []
  }
};
let stageOsAiSongMessage = "";
let stageOsVirtualMixer = {
  installed: false,
  online: false,
  message: "虚拟调音引擎按需启动",
  settings: {},
  status: {},
  devices: [],
  presets: []
};
let stageOsAiSongForm = {
  song_path: "",
  voice_model_path: "",
  voice_index_path: "",
  output_directory: "D:\\AISongStudioMVP\\data\\exports",
  quality_preset: "节省显存",
  transpose: 0,
  vocal_volume: 1,
  instrumental_volume: 0.9,
  recording_name: "我的干声",
  recording_device: 0
};
let stageOsPostLiveState = {
  source: "尚未读取本场直播记录",
  cards: [],
  history: [],
  summary: "正在等待本场直播记录。",
  operation: "idle",
  message: ""
};
const stageOsNavigation = [
  {
    id: "live-coach",
    label: "直播教练",
    defaultRoute: "live",
    items: [
      ["live", "直播现场", "&#xE7FC;"],
      ["prep", "开播准备", "&#xE9D9;"],
      ["capture", "数据采集", "&#xE968;"],
      ["analytics", "实时大盘", "&#xE9D2;"],
      ["overlay", "提示悬浮窗", "&#xE7F4;"]
    ]
  },
  {
    id: "audio-creation",
    label: "音频创作",
    defaultRoute: "audio",
    items: [
      ["audio", "音频工作台", "&#xE995;"],
      ["soundboard", "音效与 BGM", "&#xE8D6;"],
      ["song", "AI 做歌", "&#xE189;"],
      ["song-tasks", "创作任务", "&#xE9D5;"],
      ["song-files", "创作文件", "&#xED25;"]
    ]
  },
  {
    id: "review-growth",
    label: "复盘成长",
    defaultRoute: "postlive",
    items: [
      ["postlive", "本场复盘", "&#xE9D2;"],
      ["audience", "观众关系", "&#xE716;"],
      ["followup", "承接建议", "&#xE8F1;"],
      ["history", "历史报告", "&#xE81C;"]
    ]
  },
  {
    id: "system",
    label: "系统设置",
    defaultRoute: "profile",
    items: [
      ["profile", "主播档案", "&#xE77B;"],
      ["account", "账号与会员", "&#xE77B;"],
      ["connections", "设备与连接", "&#xE968;"],
      ["ai-model", "AI 与模型", "&#xE99A;"],
      ["privacy", "隐私与数据", "&#xE72E;"],
      ["diagnostics", "诊断中心", "&#xE9D9;"],
      ["about", "关于与版本", "&#xE946;"]
    ]
  }
];
let stageOsBigBoard = {
  status: "idle",
  requestId: "",
  requestedAtMs: 0,
  dataCutoffMs: 0,
  generatedAtMs: 0,
  latencyMs: 0,
  model: "",
  report: null,
  error: "",
  drawerOpen: false,
  newReminderBanner: false
};
const stageOsAudioValues = {
  electro: 24,
  pitch: 50,
  vocal: 8,
  masterVolume: 82,
  bgmVolume: 52
};

function scheduleStageOsRecoveryCodeClear(delayMs = 120000) {
  clearTimeout(stageOsRecoveryClearTimer);
  stageOsRecoveryClearTimer = null;
  if (!stageOsRecoveryCode) return;
  stageOsRecoveryClearTimer = setTimeout(() => {
    stageOsRecoveryCode = "";
    stageOsRecoveryClearTimer = null;
    if (stageOsCurrentRoute === "account") {
      stageOsAccountMessage = "恢复码已从界面内存隐藏；如已复制，系统剪贴板也会自动清理。";
      render();
    }
  }, Math.max(1000, Number(delayMs || 0)));
}

window.LiveCoachNative.receive = function receiveStageOsMessage(message) {
  const previousReminderSequence = latest?.snapshot?.sequence || latest?.snapshot?.reminder_id || "";
  stageOsBaseReceive(message);
  if (message?.type === "state" && message.payload?.commercial_account) {
    stageOsAccount = { ...stageOsAccount, ...message.payload.commercial_account };
  }
  if (message?.type === "accountState") {
    stageOsAccount = { ...stageOsAccount, ...(message.payload || {}) };
    if (message.payload?.message) stageOsAccountMessage = message.payload.message;
    stageOsAccountBusy = false;
    if (stageOsCurrentRoute === "account") render();
  }
  if (message?.type === "accountActionResult") {
    stageOsAccountBusy = false;
    stageOsAccountMessage = message.payload?.message || "";
    stageOsRecoveryCode = message.payload?.recovery_code || "";
    if (stageOsRecoveryCode) scheduleStageOsRecoveryCodeClear(120000);
    else clearTimeout(stageOsRecoveryClearTimer);
    if (message.payload?.success) {
      document.querySelectorAll("[data-account-sensitive]").forEach((input) => { input.value = ""; });
    }
    if (stageOsCurrentRoute === "account") render();
  }
  if (message?.type === "accountCopyResult") {
    stageOsAccountMessage = message.payload?.message || "";
    if (stageOsRecoveryCode) scheduleStageOsRecoveryCodeClear(60000);
    if (stageOsCurrentRoute === "account") render();
  }
  if (message?.type === "smartRackStatus") {
    const voltune = message.payload?.voltune || {};
    const soundstage = message.payload?.soundstage || {};
    if (Number.isFinite(Number(voltune.electro_amount))) {
      stageOsAudioValues.electro = Number(voltune.electro_amount);
    }
    if (Number.isFinite(Number(voltune.pitch_semitones))) {
      stageOsAudioValues.pitch = Math.round((Number(voltune.pitch_semitones) + 6) / 12 * 100);
    }
    if (Number.isFinite(Number(voltune.vocal_reduction))) {
      stageOsAudioValues.vocal = Number(voltune.vocal_reduction);
    }
    if (Number.isFinite(Number(soundstage.master_volume_percent))) {
      stageOsAudioValues.masterVolume = Number(soundstage.master_volume_percent);
    }
    if (Number.isFinite(Number(soundstage.bgm?.volume))) {
      stageOsAudioValues.bgmVolume = Math.round(Number(soundstage.bgm.volume) * 100);
    }
    const pads = Array.isArray(soundstage.pads) ? soundstage.pads : [];
    if (stageOsSelectedPadId && !pads.some((pad) => pad.id === stageOsSelectedPadId)) {
      stageOsSelectedPadId = "";
    }
  }
  if (message?.type === "overlayStatus") {
    stageOsOverlayVisible = !!message.payload?.visible;
    if (activeStep === 0 || activeStep === 5 || activeStep === 7) render();
  }
  if (message?.type === "state" && !stageOsInitialStateHandled) {
    stageOsInitialStateHandled = true;
    stageOsCurrentRoute = stageOsRouteForPage(initialPage);
    activeStep = stageOsStepForPage(initialPage);
    stageOsSessionPhase = activeStep === 8 ? "complete"
      : activeStep === 9 ? "processing"
      : "prep";
    stageOsSessionStartedAt = null;
    render();
    post("navigateRoute", { route: stageOsCurrentRoute });
    post("getSmartRackStatus");
    post("getWeChatCollectorStatus");
    post("getOverlayStatus");
    post("getAccountState");
    if (activeStep === 10) post("getAiSongStatus");
  }
  if (message?.type === "saveResult") {
    if (message.payload?.mode === "baseline") {
      stageOsSessionPhase = "prep";
      stageOsCurrentRoute = "prep";
      activeStep = 0;
    }
    if (message.payload?.mode === "plan") {
      stageOsCurrentRoute = "connections";
      activeStep = 7;
    }
    render();
  }
  if (message?.type === "bigBoardReport") {
    const payload = message.payload || {};
    stageOsBigBoard = {
      ...stageOsBigBoard,
      status: payload.status || "idle",
      requestId: payload.request_id || stageOsBigBoard.requestId,
      requestedAtMs: Number(payload.requested_at_ms || stageOsBigBoard.requestedAtMs || 0),
      dataCutoffMs: Number(payload.data_cutoff_ms || stageOsBigBoard.dataCutoffMs || 0),
      generatedAtMs: Number(payload.generated_at_ms || stageOsBigBoard.generatedAtMs || 0),
      latencyMs: Number(payload.latency_ms || stageOsBigBoard.latencyMs || 0),
      model: payload.model || stageOsBigBoard.model,
      report: payload.report || (payload.status === "loading" ? null : stageOsBigBoard.report),
      error: payload.error || "",
      drawerOpen: true,
      newReminderBanner: false
    };
    render();
  }
  if (message?.type === "openBigBoard") {
    const payload = message.payload || {};
    if (payload.report || payload.status) {
      stageOsBigBoard = {
        ...stageOsBigBoard,
        status: payload.status || stageOsBigBoard.status,
        report: payload.report || stageOsBigBoard.report,
        dataCutoffMs: Number(payload.data_cutoff_ms || stageOsBigBoard.dataCutoffMs || 0),
        generatedAtMs: Number(payload.generated_at_ms || stageOsBigBoard.generatedAtMs || 0),
        latencyMs: Number(payload.latency_ms || stageOsBigBoard.latencyMs || 0),
        model: payload.model || stageOsBigBoard.model,
        error: payload.error || stageOsBigBoard.error
      };
    }
    stageOsBigBoard.drawerOpen = true;
    stageOsCurrentRoute = "analytics";
    activeStep = stageOsSessionPhase === "live" ? 5 : 0;
    render();
  }
  if (message?.type === "latest" && stageOsBigBoard.drawerOpen && stageOsBigBoard.status === "ready") {
    const nextReminderSequence = latest?.snapshot?.sequence || latest?.snapshot?.reminder_id || "";
    if (nextReminderSequence && previousReminderSequence && nextReminderSequence !== previousReminderSequence) {
      stageOsBigBoard.newReminderBanner = true;
      render();
    }
  }
  if (message?.type === "postLiveCoachState") {
    stageOsPostLiveState = {
      ...stageOsPostLiveState,
      ...(message.payload || {}),
      cards: Array.isArray(message.payload?.cards) ? message.payload.cards : [],
      history: Array.isArray(message.payload?.history) ? message.payload.history : []
    };
    stageOsPostLiveLoading = false;
    if (stageOsPostLiveSelectedIndex >= stageOsPostLiveState.cards.length) {
      stageOsPostLiveSelectedIndex = Math.max(0, stageOsPostLiveState.cards.length - 1);
    }
    const selectedCard = stageOsPostLiveState.cards[stageOsPostLiveSelectedIndex];
    if (stageOsPostLiveSelectedOpener >= (selectedCard?.openers?.length || 0)) {
      stageOsPostLiveSelectedOpener = 0;
    }
    if (["postlive", "audience", "followup", "history"].includes(stageOsCurrentRoute)) render();
  }
  if (message?.type === "audioSettingsResult") {
    stageOsAudioSettingsMessage = message.payload?.message || "";
    if (["audio", "soundboard"].includes(stageOsCurrentRoute)) render();
  }
  if (message?.type === "demoState") {
    stageOsDemoState = {
      ...stageOsDemoState,
      ...(message.payload || {}),
      state: { ...stageOsDemoState.state, ...(message.payload?.state || {}) },
      scenarios: Array.isArray(message.payload?.scenarios) ? message.payload.scenarios : []
    };
    if (stageOsCurrentRoute === "diagnostics") render();
  }
  if (message?.type === "aiSongStatus") {
    const worker = message.payload?.worker || {};
    if (worker.default_output_directory && !stageOsAiSongForm.output_directory) {
      stageOsAiSongForm.output_directory = worker.default_output_directory;
    }
    if (["song", "song-tasks", "song-files"].includes(stageOsCurrentRoute)) render();
  }
  if (message?.type === "aiSongFileSelected") {
    const kind = message.payload?.kind;
    const key = kind === "model" ? "voice_model_path"
      : kind === "index" ? "voice_index_path"
      : kind === "output" ? "output_directory"
      : "song_path";
    stageOsAiSongForm[key] = message.payload?.path || "";
    render();
  }
  if (message?.type === "aiSongActionResult") {
    stageOsAiSongMessage = message.payload?.message || "";
    render();
  }
  if (message?.type === "virtualMixerState") {
    stageOsVirtualMixer = { ...stageOsVirtualMixer, ...(message.payload || {}) };
    if (["audio", "soundboard", "diagnostics"].includes(stageOsCurrentRoute)) render();
  }
  if (message?.type === "safeExitRequired") {
    stageOsSafeExit = message.payload || {};
    render();
  }
};

render = function renderStageOs() {
  document.body.className = [
    "stage-os",
    `is-route-${stageOsCurrentRoute}`,
    activeStep === 5 ? "is-live-page" : "",
    activeStep === 6 ? "is-audio-page" : "",
    activeStep === 8 ? "is-postlive-page" : "",
    activeStep === 9 ? "is-processing-page" : "",
    activeStep === 10 ? "is-ai-song-page" : "",
    stageOsSessionPhase !== "live" && activeStep === 0 ? "is-preflight-page" : ""
  ].filter(Boolean).join(" ");
  renderRail();
  renderPrep();
  renderTrust();
  renderStage();
};

renderRail = function renderStageOsRail() {
  const activeSection = stageOsSectionForRoute(stageOsCurrentRoute);
  $("#main-menu").innerHTML = stageOsNavigation.map((section) => `
    <button class="os-main-menu-item ${section.id === activeSection.id ? "active" : ""}"
            data-stage-nav="${section.defaultRoute}">${section.label}</button>
  `).join("");
  if (stageOsSessionPhase !== "live" && activeStep === 0) {
    $("#sidebar-heading").innerHTML = `
      <span>开播前检查</span>
      <small>必要项就绪后再进入直播</small>
    `;
    $("#step-rail").innerHTML = stageOsPreflightRail();
    $("#sidebar-status").innerHTML = stageOsPreflightSidebarFooter();
    wireStageOsNavigation();
    return;
  }
  $("#sidebar-heading").innerHTML = `
    <span>${activeSection.label}</span>
    <small>${stageOsRouteDescription(activeSection.id)}</small>
  `;
  $("#step-rail").innerHTML = activeSection.items.map(([route, label, icon]) => `
    <button class="os-sidebar-item ${stageOsCurrentRoute === route ? "active" : ""}"
            data-stage-nav="${route}"><i>${icon}</i><span>${label}</span></button>
  `).join("");
  $("#sidebar-status").innerHTML = stageOsSidebarStatus();
  wireStageOsNavigation();
};

renderPrep = function hideLegacyPrep() {
  $("#prep-card").innerHTML = "";
};

renderTrust = function hideLegacyTrust() {
  $("#trust-strip").innerHTML = "";
};

renderStage = function renderStageOsPage() {
  const page = stageOsPageForRoute(stageOsCurrentRoute);
  $("#stage").innerHTML = page() + stageOsSafeExitModal() + stageOsPadDeleteModal() + stageOsAccountLogoutModal();
  wireInputs();
  wirePageActions();
  drawStageOsWaveforms();
};

wirePageActions = function wireStageOsPageActions() {
  stageOsBaseWirePageActions();
  document.querySelector("[data-account-refresh]")?.addEventListener("click", () => {
    stageOsAccountBusy = true;
    stageOsAccountMessage = "正在刷新账号和额度…";
    render();
    post("getAccountState");
  });
  document.querySelector("[data-account-logout]")?.addEventListener("click", () => {
    if (stageOsAccountBusy) return;
    stageOsAccountLogoutConfirmOpen = true;
    render();
  });
  document.querySelector("[data-cancel-account-logout]")?.addEventListener("click", () => {
    stageOsAccountLogoutConfirmOpen = false;
    render();
  });
  document.querySelector("[data-confirm-account-logout]")?.addEventListener("click", () => {
    stageOsAccountLogoutConfirmOpen = false;
    stageOsAccountBusy = true;
    stageOsRecoveryCode = "";
    clearTimeout(stageOsRecoveryClearTimer);
    stageOsAccountMessage = "正在退出并清除本机授权…";
    render();
    post("accountLogout");
  });
  document.querySelector("[data-copy-account-recovery]")?.addEventListener("click", () => {
    if (!stageOsRecoveryCode) return;
    post("copyAccountRecoveryCode", { recovery_code: stageOsRecoveryCode });
  });
  document.querySelector("[data-account-recover]")?.addEventListener("submit", (event) => {
    event.preventDefault();
    if (stageOsAccountBusy) return;
    const formElement = event.currentTarget;
    stageOsAccountBusy = true;
    stageOsRecoveryCode = "";
    stageOsAccountMessage = "正在恢复账号并绑定本机…";
    post("accountRecover", {
      recovery_code: formElement.querySelector('[name="recovery_code"]')?.value || ""
    });
    render();
  });
  document.querySelector("[data-account-activate]")?.addEventListener("submit", (event) => {
    event.preventDefault();
    if (stageOsAccountBusy) return;
    const formElement = event.currentTarget;
    stageOsAccountBusy = true;
    stageOsRecoveryCode = "";
    stageOsAccountMessage = "正在验证激活码并创建轻账号…";
    post("accountActivate", {
      display_name: formElement.querySelector('[name="display_name"]')?.value || "",
      activation_code: formElement.querySelector('[name="activation_code"]')?.value || ""
    });
    render();
  });
  document.querySelector("[data-start-session]")?.addEventListener("click", () => startStageOsSession(false));
  document.querySelector("[data-start-local-session]")?.addEventListener("click", () => startStageOsSession(true));
  document.querySelector("[data-toggle-preflight-audio]")?.addEventListener("click", () => {
    stageOsPreflightAudioOpen = !stageOsPreflightAudioOpen;
    render();
  });
  document.querySelectorAll("[data-preflight-recheck]").forEach((button) => {
    button.addEventListener("click", () => {
      post("getLatest", form);
      post("getSmartRackStatus");
      post("getWeChatCollectorStatus");
      post("getOverlayStatus");
    });
  });
  document.querySelectorAll("[data-end-session]").forEach((button) => {
    button.addEventListener("click", () => {
      stageOsEndConfirmOpen = true;
      render();
    });
  });
  document.querySelector("[data-cancel-end]")?.addEventListener("click", () => {
    stageOsEndConfirmOpen = false;
    render();
  });
  document.querySelector("[data-confirm-end]")?.addEventListener("click", endStageOsSession);
  document.querySelector("[data-cancel-safe-exit]")?.addEventListener("click", () => {
    stageOsSafeExit = null;
    render();
  });
  document.querySelector("[data-confirm-safe-exit]")?.addEventListener("click", () => {
    post("confirmSafeExit");
  });
  document.querySelector("[data-cancel-pad-delete]")?.addEventListener("click", () => {
    stageOsPendingPadDeleteId = "";
    render();
  });
  document.querySelector("[data-confirm-pad-delete]")?.addEventListener("click", () => {
    const padId = stageOsPendingPadDeleteId;
    stageOsPendingPadDeleteId = "";
    stageOsSelectedPadId = "";
    if (padId) post("audioQuickAction", { action: "deletePad", value: padId });
    render();
  });
  document.querySelector("[data-pause-reminders]")?.addEventListener("click", () => {
    stageOsRemindersPaused = !stageOsRemindersPaused;
    render();
  });
  document.querySelectorAll("[data-open-audio]").forEach((button) => {
    button.addEventListener("click", () => {
      activeStep = 6;
      render();
      post("getSmartRackStatus");
    });
  });
  document.querySelector("[data-open-ai-song]")?.addEventListener("click", () => {
    post("openAiSongStudio", { live_session: stageOsSessionPhase === "live" });
    setTimeout(() => post("getAiSongStatus"), 900);
  });
  document.querySelector("[data-refresh-ai-song]")?.addEventListener("click", () => {
    post("getAiSongStatus");
  });
  document.querySelectorAll("[data-choose-ai-song]").forEach((button) => {
    button.addEventListener("click", () => post("chooseAiSongFile", { kind: button.dataset.chooseAiSong }));
  });
  document.querySelectorAll("[data-ai-song-field]").forEach((input) => {
    input.addEventListener("input", () => {
      const key = input.dataset.aiSongField;
      stageOsAiSongForm[key] = input.type === "number" || input.type === "range"
        ? Number(input.value)
        : input.value;
      const valueLabel = input.closest("label")?.querySelector("b");
      if (valueLabel && input.type === "range") valueLabel.textContent = Number(input.value).toFixed(2);
    });
  });
  document.querySelector("[data-start-ai-song-job]")?.addEventListener("click", () => {
    stageOsAiSongMessage = "正在提交任务…";
    post("aiSongCommand", { action: "startJob", live_session: stageOsSessionPhase === "live", ...stageOsAiSongForm });
    render();
  });
  document.querySelector("[data-cancel-ai-song-job]")?.addEventListener("click", () =>
    post("aiSongCommand", { action: "cancelJob" }));
  document.querySelector("[data-start-ai-recording]")?.addEventListener("click", () =>
    post("aiSongCommand", {
      action: "startRecording",
      live_session: stageOsSessionPhase === "live",
      device_number: Number(stageOsAiSongForm.recording_device || 0),
      name: stageOsAiSongForm.recording_name || "我的干声",
      output_directory: aiSongStatus?.worker?.recordings_directory || aiSongStatus?.recordings_directory
    }));
  document.querySelector("[data-stop-ai-recording]")?.addEventListener("click", () =>
    post("aiSongCommand", { action: "stopRecording" }));
  document.querySelectorAll("[data-vm-field]").forEach((input) => {
    input.addEventListener("input", () => {
      stageOsVirtualMixer.settings[input.dataset.vmField] = input.dataset.vmField === "pitch_scale"
        ? Number(input.value)
        : input.type === "checkbox"
        ? input.checked
        : input.type === "number" || input.type === "range"
          ? Number(input.value)
          : input.value || null;
    });
  });
  document.querySelectorAll("[data-vm-action]").forEach((button) => {
    button.addEventListener("click", () => {
      const action = button.dataset.vmAction;
      post("virtualMixerAction", {
        action,
        settings: stageOsVirtualMixer.settings,
        bypass: action === "bypass" ? !stageOsVirtualMixer.status?.is_bypassed : undefined
      });
    });
  });
  document.querySelectorAll("[data-vm-preset]").forEach((button) => {
    button.addEventListener("click", () => post("virtualMixerAction", { action: "preset", preset: button.dataset.vmPreset }));
  });
  document.querySelectorAll("[data-audio-mode]").forEach((button) => {
    button.addEventListener("click", () => {
      stageOsAudioModeDraft = button.dataset.audioMode || "none";
      render();
    });
  });
  document.querySelectorAll("[data-configure-audio]").forEach((button) => {
    button.addEventListener("click", () => {
      const mode = stageOsAudioModeDraft || smartRackStatus?.mode || "none";
      stageOsAudioModeDraft = null;
      post("configureAudioAssistants", { mode });
      setTimeout(() => post("getSmartRackStatus"), 1200);
    });
  });
  document.querySelector("[data-refresh-prep]")?.addEventListener("click", () => {
    post("getLatest", form);
    post("getSmartRackStatus");
    post("getWeChatCollectorStatus");
    post("getOverlayStatus");
  });
  document.querySelector("[data-save-stage-profile]")?.addEventListener("click", () => {
    if (!validateStep()) return;
    post("saveBaseline", buildSubmissionPayload());
  });
  document.querySelector("[data-save-stage-settings]")?.addEventListener("click", () => {
    post("savePlan", buildSubmissionPayload());
  });
  document.querySelector("[data-view-full-postlive]")?.addEventListener("click", () => goStageOsRoute("postlive"));
  document.querySelectorAll("[data-postlive-source]").forEach((button) => {
    button.addEventListener("click", () => {
      stageOsPostLiveLoading = true;
      post("getPostLiveCoach", { source: button.dataset.postliveSource || "latest" });
      render();
    });
  });
  document.querySelectorAll("[data-postlive-card]").forEach((button) => {
    button.addEventListener("click", () => {
      stageOsPostLiveSelectedIndex = Number(button.dataset.postliveCard || 0);
      stageOsPostLiveSelectedOpener = 0;
      render();
    });
  });
  document.querySelectorAll("[data-postlive-opener]").forEach((button) => {
    button.addEventListener("click", () => {
      stageOsPostLiveSelectedOpener = Number(button.dataset.postliveOpener || 0);
      render();
    });
  });
  document.querySelector("[data-postlive-refine]")?.addEventListener("click", () => {
    if (stageOsPostLiveLoading) return;
    stageOsPostLiveLoading = true;
    post("refinePostLiveOpener", { card_index: stageOsPostLiveSelectedIndex });
    render();
  });
  document.querySelector("[data-postlive-copy]")?.addEventListener("click", () => {
    post("copyPostLiveOpener", {
      card_index: stageOsPostLiveSelectedIndex,
      opener_index: stageOsPostLiveSelectedOpener
    });
  });
  document.querySelectorAll("[data-postlive-outcome]").forEach((button) => {
    button.addEventListener("click", () => post("recordPostLiveOutcome", {
      card_index: stageOsPostLiveSelectedIndex,
      outcome: button.dataset.postliveOutcome
    }));
  });
  document.querySelectorAll("[data-big-board-button]").forEach((button) => {
    button.addEventListener("click", () => {
      if (stageOsBigBoard.status === "loading") return;
      if (stageOsBigBoard.status === "ready" && stageOsBigBoard.report) {
        stageOsBigBoard.drawerOpen = true;
        stageOsBigBoard.newReminderBanner = false;
        render();
        return;
      }
      requestStageOsBigBoard();
    });
  });
  document.querySelector("[data-big-board-regenerate]")?.addEventListener("click", requestStageOsBigBoard);
  document.querySelectorAll("[data-big-board-close]").forEach((button) => {
    button.addEventListener("click", () => {
      stageOsBigBoard.drawerOpen = false;
      stageOsBigBoard.newReminderBanner = false;
      render();
    });
  });
  document.querySelector("[data-big-board-reminder]")?.addEventListener("click", () => {
    stageOsBigBoard.drawerOpen = false;
    stageOsBigBoard.newReminderBanner = false;
    render();
  });
  document.querySelectorAll("[data-audio-preset]").forEach((button) => {
    button.addEventListener("click", () => {
      document.querySelectorAll("[data-audio-preset]").forEach((item) => item.classList.remove("selected"));
      button.classList.add("selected");
      post("audioQuickAction", { action: "preset", value: button.dataset.audioPreset });
    });
  });
  document.querySelectorAll("[data-sound-pad]").forEach((button) => {
    button.addEventListener("click", () => {
      post("audioQuickAction", { action: "pad", value: button.dataset.soundPad });
      button.classList.add("playing");
      setTimeout(() => button.classList.remove("playing"), 560);
    });
  });
  document.querySelectorAll("[data-pad-id]").forEach((button) => {
    button.addEventListener("click", () => {
      post("audioQuickAction", { action: "padId", value: button.dataset.padId });
      button.classList.add("playing");
      setTimeout(() => button.classList.remove("playing"), 560);
    });
  });
  document.querySelectorAll("[data-stop-pad]").forEach((button) => {
    button.addEventListener("click", () => post("audioQuickAction", {
      action: "stopPad",
      value: button.dataset.stopPad
    }));
  });
  document.querySelectorAll("[data-edit-pad]").forEach((button) => {
    button.addEventListener("click", () => {
      stageOsSelectedPadId = button.dataset.editPad || "";
      render();
    });
  });
  const applySoundPadFilter = () => {
    const query = String(document.querySelector("[data-audio-search]")?.value || "").trim().toLowerCase();
    const favoritesOnly = !!document.querySelector("[data-audio-favorites]")?.checked;
    stageOsAudioSearch = query;
    stageOsAudioFavoriteOnly = favoritesOnly;
    document.querySelectorAll("[data-sound-pad-card]").forEach((card) => {
      const matchesQuery = !query || String(card.dataset.searchText || "").toLowerCase().includes(query);
      const matchesFavorite = !favoritesOnly || card.dataset.favorite === "true";
      card.hidden = !(matchesQuery && matchesFavorite);
    });
  };
  document.querySelector("[data-audio-search]")?.addEventListener("input", applySoundPadFilter);
  document.querySelector("[data-audio-favorites]")?.addEventListener("change", applySoundPadFilter);
  document.querySelector("[data-voltune-save-route]")?.addEventListener("click", () => {
    const input = document.querySelector("[data-voltune-input]");
    post("audioQuickAction", {
      action: "voltuneDevices",
      input_id: Number(input?.value),
      input_name: input?.selectedOptions?.[0]?.textContent?.trim() || "当前麦克风",
      output_id: Number(document.querySelector("[data-voltune-output]")?.value)
    });
  });
  document.querySelector("[data-voltune-start]")?.addEventListener("click", () => {
    const input = document.querySelector("[data-voltune-input]");
    post("audioQuickAction", {
      action: "voltuneStart",
      input_id: Number(input?.value),
      input_name: input?.selectedOptions?.[0]?.textContent?.trim() || "当前麦克风",
      output_id: Number(document.querySelector("[data-voltune-output]")?.value)
    });
  });
  document.querySelector("[data-voltune-stop]")?.addEventListener("click", () =>
    post("audioQuickAction", { action: "voltuneStop" }));
  document.querySelector("[data-start-voiceprint]")?.addEventListener("click", () => {
    const input = document.querySelector("[data-voltune-input]");
    post("startVoiceprintCalibration", {
      device_id: Number(input?.value),
      device_name: input?.selectedOptions?.[0]?.textContent?.trim() || "当前麦克风"
    });
  });
  document.querySelector("[data-cancel-voiceprint]")?.addEventListener("click", () =>
    post("cancelVoiceprintCalibration"));
  document.querySelector("[data-save-audio-settings]")?.addEventListener("click", () => {
    const host = document.querySelector("[data-audio-settings]");
    if (!host) return;
    stageOsAudioSettingsMessage = "正在保存…";
    post("saveAudioSettings", {
      voltune_executable: host.querySelector("[data-audio-voltune-path]")?.value || "",
      soundstage_executable: host.querySelector("[data-audio-soundstage-path]")?.value || "",
      status_refresh_seconds: Number(host.querySelector("[data-audio-refresh-seconds]")?.value || 3),
      keep_voltune_on_exit: !!host.querySelector("[data-audio-keep-voltune]")?.checked,
      keep_soundstage_on_exit: !!host.querySelector("[data-audio-keep-soundstage]")?.checked
    });
    render();
  });
  document.querySelectorAll("[data-demo-action]").forEach((button) => {
    button.addEventListener("click", () => {
      const host = document.querySelector("[data-demo-controller]");
      post("demoAction", {
        action: button.dataset.demoAction,
        scenario: host?.querySelector("[data-demo-scenario]")?.value || "new-viewer",
        mode: host?.querySelector("[data-demo-mode]")?.value || "fixed",
        delay_seconds: Number(host?.querySelector("[data-demo-delay]")?.value || 1.8)
      });
    });
  });
  document.querySelector("[data-sound-output]")?.addEventListener("change", (event) =>
    post("audioQuickAction", { action: "soundOutput", value: event.target.value }));
  document.querySelector("[data-audio-ducking]")?.addEventListener("change", (event) =>
    post("audioQuickAction", { action: "ducking", value: event.target.checked }));
  document.querySelector("[data-stop-all-sounds]")?.addEventListener("click", () =>
    post("audioQuickAction", { action: "stopAll" }));
  document.querySelector("[data-import-audio]")?.addEventListener("click", () =>
    post("audioQuickAction", { action: "importAudio" }));
  document.querySelector("[data-import-bgm]")?.addEventListener("click", () =>
    post("audioQuickAction", { action: "importBgm" }));
  document.querySelector("[data-toggle-bgm]")?.addEventListener("click", (event) =>
    post("audioQuickAction", { action: "bgmPlayback", value: event.currentTarget.dataset.playing !== "true" }));
  document.querySelector("[data-save-pad-settings]")?.addEventListener("click", () => {
    const host = document.querySelector("[data-pad-editor]");
    if (!host) return;
    post("audioQuickAction", {
      action: "padSettings",
      pad_id: host.dataset.padEditor,
      name: host.querySelector("[data-pad-name]")?.value || "音效",
      category: host.querySelector("[data-pad-category]")?.value || "自定义",
      shortcut: host.querySelector("[data-pad-shortcut]")?.value || "",
      color: host.querySelector("[data-pad-color]")?.value || "blue",
      volume: Number(host.querySelector("[data-pad-volume]")?.value || 80),
      mode: host.querySelector("[data-pad-mode]")?.value || "restart",
      favorite: !!host.querySelector("[data-pad-favorite]")?.checked
    });
  });
  document.querySelector("[data-delete-pad]")?.addEventListener("click", (event) => {
    const padId = event.currentTarget.dataset.deletePad;
    if (!padId) return;
    stageOsPendingPadDeleteId = padId;
    render();
  });
  document.querySelectorAll("[data-audio-range]").forEach((input) => {
    const syncRangeVisual = () => {
      const min = Number(input.min || 0);
      const max = Number(input.max || 100);
      const value = Number(input.value);
      stageOsAudioValues[input.dataset.audioRange] = value;
      const percent = max === min ? 0 : ((value - min) / (max - min)) * 100;
      const control = input.closest(".os-range-control");
      const rangeHost = control || input.closest("label");
      rangeHost?.style.setProperty("--value", `${Math.max(0, Math.min(100, percent))}%`);
      const valueLabel = control?.querySelector("b") || input.closest("label")?.querySelector("b");
      if (valueLabel) {
        if (input.dataset.audioRange === "pitch") {
          const semitones = Math.round((value / 100) * 12 - 6);
          valueLabel.textContent = semitones > 0 ? `+${semitones}` : `${semitones}`;
        } else {
          valueLabel.textContent = `${Math.round(value)}%`;
        }
      }
    };
    syncRangeVisual();
    input.addEventListener("input", syncRangeVisual);
    input.addEventListener("change", () => {
      syncRangeVisual();
      post("audioQuickAction", {
        action: input.dataset.audioRange,
        value: Number(input.value)
      });
    });
  });
  document.querySelector("[data-open-collector-inline]")?.addEventListener("click", () => {
    goStageOsRoute("capture");
  });
  document.querySelectorAll("[data-toggle-live-data]").forEach((button) => {
    button.addEventListener("click", () => {
      if (["session_lost", "stalled"].includes(wechatCollectorStatus.collector_state)) {
        goStageOsRoute("capture");
        return;
      }
      const enabled = !wechatCollectorStatus.collector_attached;
      post("setWeChatCollector", { enabled });
      setTimeout(() => post("getWeChatCollectorStatus"), 700);
    });
  });
  document.querySelectorAll("[data-media-capture-toggle]").forEach((input) => {
    input.addEventListener("change", () => {
      post("setMediaCapture", { enabled: input.checked });
      setTimeout(() => post("getWeChatCollectorStatus"), 700);
    });
  });
  document.querySelectorAll("[data-toggle-overlay]").forEach((button) => {
    button.addEventListener("click", () => {
      const enabled = !stageOsOverlayVisible;
      post("setOverlay", { ...buildSubmissionPayload(), enabled });
    });
  });
  document.querySelectorAll("#stage [data-stage-nav]").forEach((button) => {
    button.addEventListener("click", () => goStageOsRoute(button.dataset.stageNav));
  });
};

function wireStageOsNavigation() {
  document.querySelectorAll("[data-stage-nav]").forEach((button) => {
    button.addEventListener("click", () => goStageOsRoute(button.dataset.stageNav));
  });
  document.querySelector("[data-dismiss-notice]")?.addEventListener("click", () => {
    pageNotice = { message: "", kind: "info" };
    render();
  });
}

function goStageOsRoute(route) {
  stageOsAvatarMenuOpen = false;
  stageOsCurrentRoute = stageOsNormalizeRoute(route);
  if (stageOsCurrentRoute === "prep" && stageOsSessionPhase !== "live") {
    stageOsSessionPhase = "prep";
    stageOsSessionStartedAt = null;
    stageOsLocalModeActive = false;
    stageOsPreflightAudioOpen = false;
  }
  activeStep = stageOsStepForRoute(stageOsCurrentRoute);
  if (stageOsCurrentRoute === "analytics") {
    stageOsBigBoard.drawerOpen = true;
  }
  render();
  post("navigateRoute", { route: stageOsCurrentRoute });
  if (activeStep === 6) {
    post("getSmartRackStatus");
    post("getVirtualMixerState");
  }
  if (activeStep === 10) post("getAiSongStatus");
  if (stageOsCurrentRoute === "account") post("getAccountState");
  if (["postlive", "audience", "followup", "history"].includes(stageOsCurrentRoute)) {
    stageOsPostLiveLoading = true;
    post("getPostLiveCoach", { source: "latest" });
  }
  if (["live", "prep", "capture", "analytics", "overlay", "connections", "diagnostics"].includes(stageOsCurrentRoute)) {
    post("getWeChatCollectorStatus");
    post("getOverlayStatus");
  }
  if (stageOsCurrentRoute === "diagnostics") {
    post("getDemoState");
    post("getAiSongStatus");
    post("getSmartRackStatus");
    post("getVirtualMixerState");
  }
}

function stageOsNormalizeRoute(route) {
  const value = String(route || "").trim().toLowerCase();
  const aliases = {
    "ai-song": "song",
    "smart-rack": "audio",
    "settings": "connections",
    "wechat-collector": "capture",
    "post-live-coach": "postlive",
    "processing": "postlive"
  };
  const normalized = aliases[value] || value;
  return stageOsNavigation.some((section) => section.items.some(([itemRoute]) => itemRoute === normalized))
    ? normalized
    : "prep";
}

function stageOsRouteForPage(page) {
  return stageOsNormalizeRoute(page || "prep");
}

function stageOsSectionForRoute(route) {
  return stageOsNavigation.find((section) => section.items.some(([itemRoute]) => itemRoute === route))
    || stageOsNavigation[0];
}

function stageOsStepForRoute(route) {
  if (["audio", "soundboard"].includes(route)) return 6;
  if (["song", "song-tasks", "song-files"].includes(route)) return 10;
  if (["postlive", "audience", "followup", "history"].includes(route)) return 8;
  if (route === "profile") return 1;
  if (["account", "connections", "ai-model", "privacy", "diagnostics", "about", "capture"].includes(route)) return 7;
  return stageOsSessionPhase === "live" ? 5 : 0;
}

function stageOsPageForRoute(route) {
  if (stageOsSessionPhase === "processing") return stageOsProcessingPage;
  if (["audio", "soundboard"].includes(route)) return stageOsAudioPage;
  if (["song", "song-tasks", "song-files"].includes(route)) return stageOsAiSongPage;
  if (["postlive", "audience", "followup", "history"].includes(route)) return stageOsPostLivePage;
  if (route === "profile") return stageOsProfilePage;
  if (["account", "connections", "ai-model", "privacy", "diagnostics", "about", "capture"].includes(route)) return stageOsSettingsPage;
  return stageOsLivePage;
}

function stageOsRouteDescription(sectionId) {
  if (sectionId === "audio-creation") return "声音控制与内容创作";
  if (sectionId === "review-growth") return "复盘、关系与下场行动";
  if (sectionId === "system") return "档案、连接、安全与诊断";
  return stageOsSessionPhase === "live" ? "直播进行中" : "开播前准备与实时教练";
}

function stageOsSidebarStatus() {
  const collectorOnline = !!wechatCollectorStatus?.collector_attached;
  const analysisOnline = !!wechatCollectorStatus?.analysis_running;
  const audioMode = smartRackStatus?.mode || "none";
  return `
    <div><span class="${analysisOnline ? "online" : ""}"></span><b>分析服务</b><small>${analysisOnline ? "运行中" : "待启动"}</small></div>
    <div><span class="${collectorOnline ? "online" : ""}"></span><b>数据采集</b><small>${collectorOnline ? "已连接" : "未连接"}</small></div>
    <div><span class="${audioMode !== "none" ? "online" : "optional"}"></span><b>音频能力</b><small>${audioMode !== "none" ? "已启用" : "可选"}</small></div>
    ${pageNotice?.message ? `<p class="os-sidebar-notice warning"><span>${escapeHtml(pageNotice.message)}</span><button data-dismiss-notice aria-label="关闭提示">×</button></p>` : ""}
  `;
}

function stageOsStepForPage(page) {
  return stageOsStepForRoute(stageOsRouteForPage(page));
}

function startStageOsSession(localMode = false) {
  const preflight = stageOsPreflightState();
  if (!localMode && !preflight.requiredReady) return;
  stageOsLocalModeActive = localMode === true && !preflight.requiredReady;
  stageOsPreflightAudioOpen = false;
  stageOsSessionPhase = "live";
  stageOsCurrentRoute = "live";
  stageOsSessionStartedAt = Date.now();
  activeStep = 5;
  stageOsEndConfirmOpen = false;
  stageOsBigBoard = {
    status: "idle",
    requestId: "",
    requestedAtMs: 0,
    dataCutoffMs: 0,
    generatedAtMs: 0,
    latencyMs: 0,
    model: "",
    report: null,
    error: "",
    drawerOpen: false,
    newReminderBanner: false
  };
  post("getLatest", form);
  post("getSmartRackStatus");
  post("getWeChatCollectorStatus");
  render();
}

function requestStageOsBigBoard() {
  if (stageOsBigBoard.status === "loading") return;
  stageOsBigBoard = {
    ...stageOsBigBoard,
    status: "loading",
    requestedAtMs: Date.now(),
    dataCutoffMs: Date.now(),
    report: null,
    error: "",
    drawerOpen: true,
    newReminderBanner: false
  };
  render();
  post("requestBigBoard", form);
}

function endStageOsSession() {
  stageOsEndConfirmOpen = false;
  stageOsSessionPhase = "processing";
  stageOsCurrentRoute = "postlive";
  activeStep = 9;
  clearTimeout(stageOsProcessingTimer);
  render();
  stageOsProcessingTimer = setTimeout(() => {
    stageOsSessionPhase = "complete";
    stageOsCurrentRoute = "postlive";
    activeStep = 8;
    render();
  }, 2200);
}

function stageOsLiveHeaderStatus() {
  const snapshot = latest?.snapshot || {};
  const { viewers, comments, gifts } = stageOsLiveMetrics(snapshot);
  const aiReady = !!wechatCollectorStatus.analysis_running && latest?.connected !== false;
  return `
    <div class="os-live-header-status">
      <span class="primary"><i></i>正在直播 <time data-session-time>${stageOsElapsed()}</time></span>
      <span class="${aiReady ? "ready" : ""}"><i class="os-fluent">&#xE945;</i>${aiReady ? "AI 提醒在线" : "AI 等待音画采集"}</span>
      ${stageOsLocalModeActive ? '<span class="degraded"><i class="os-fluent">&#xE7BA;</i>本地模式 · 部分能力受限</span>' : ""}
      <b>在线 ${viewers}　评论 ${comments}　礼物 ${gifts}</b>
      ${stageOsBigBoardButton()}
    </div>
  `;
}

function stageOsBigBoardButton() {
  const readiness = stageOsBigBoardReadiness();
  const readyReport = stageOsBigBoard.status === "ready" && !!stageOsBigBoard.report;
  const loading = stageOsBigBoard.status === "loading";
  const error = stageOsBigBoard.status === "error";
  const label = loading ? "分析中…"
    : readyReport ? "查看大盘"
      : error ? "重新分析"
        : readiness.ready ? "大盘分析"
          : readiness.label;
  return `
    <button class="os-big-board-button ${readyReport ? "ready" : ""} ${error ? "error" : ""}" data-big-board-button
      ${loading || (!readiness.ready && !readyReport && !error) ? "disabled" : ""}
      aria-label="${escapeAttr(label)}">
      ${readyReport ? "<i></i>" : ""}<span>${escapeHtml(label)}</span>
    </button>
  `;
}

function stageOsBigBoardReadiness() {
  const elapsedSeconds = stageOsSessionStartedAt
    ? Math.max(0, Math.floor((Date.now() - stageOsSessionStartedAt) / 1000))
    : 0;
  const hasData = Number(wechatCollectorStatus.last_snapshot_at_ms || 0) > 0 ||
    !!latest?.snapshot ||
    Number(wechatCollectorStatus.updated_at_ms || 0) > 0;
  if (!hasData) return { ready: false, label: "等待直播数据" };
  if (elapsedSeconds < 300) {
    const rest = 300 - elapsedSeconds;
    return { ready: false, label: `数据积累中 ${String(Math.floor(rest / 60)).padStart(2, "0")}:${String(rest % 60).padStart(2, "0")}` };
  }
  return { ready: true, label: "大盘分析" };
}

function stageOsLiveMetrics(snapshot = {}) {
  const collectorHasSnapshot = Number(wechatCollectorStatus.last_snapshot_at_ms || 0) > 0;
  const collectedComments = Math.max(
    Number(wechatCollectorStatus.cumulative_comments || 0),
    Number(wechatCollectorStatus.comments || 0)
  );
  return {
    viewers: Number(collectorHasSnapshot ? wechatCollectorStatus.viewers ?? 0 : snapshot.viewers ?? wechatCollectorStatus.viewers ?? 0),
    comments: Number(collectorHasSnapshot ? collectedComments : snapshot.comments ?? collectedComments),
    gifts: Number(collectorHasSnapshot ? wechatCollectorStatus.gifts ?? 0 : snapshot.gifts ?? wechatCollectorStatus.gifts ?? 0)
  };
}

function stageOsCommentClock(timestampMs) {
  const value = Number(timestampMs || 0);
  if (!Number.isFinite(value) || value <= 0) return "评论";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "评论";
  return date.toLocaleTimeString("zh-CN", {
    hour: "2-digit",
    minute: "2-digit",
    hour12: false
  });
}

function stageOsElapsed() {
  if (!stageOsSessionStartedAt) return "00:00";
  const seconds = Math.max(0, Math.floor((Date.now() - stageOsSessionStartedAt) / 1000));
  const minutes = Math.floor(seconds / 60);
  const rest = seconds % 60;
  return `${String(minutes).padStart(2, "0")}:${String(rest).padStart(2, "0")}`;
}

setInterval(() => {
  const target = document.querySelector("[data-session-time]");
  if (target && stageOsSessionPhase === "live") target.textContent = stageOsElapsed();
  const bigBoardButton = document.querySelector("[data-big-board-button]");
  if (bigBoardButton && !["loading", "ready", "error"].includes(stageOsBigBoard.status)) {
    const readiness = stageOsBigBoardReadiness();
    bigBoardButton.disabled = !readiness.ready;
    const label = bigBoardButton.querySelector("span");
    if (label) label.textContent = readiness.label;
  }
}, 1000);

function stageOsPreflightAge(timestampMs) {
  const value = Number(timestampMs || 0);
  if (!Number.isFinite(value) || value <= 0) return "尚未收到状态";
  const seconds = Math.max(0, Math.round((Date.now() - value) / 1000));
  if (seconds < 5) return "刚刚";
  if (seconds < 60) return `${seconds} 秒前`;
  const minutes = Math.floor(seconds / 60);
  if (minutes < 60) return `${minutes} 分钟前`;
  return stageOsFormatDateTime(value);
}

function stageOsPreflightState() {
  const collectorState = firstText(wechatCollectorStatus.collector_state) || "not_started";
  const collectorAttached = !!wechatCollectorStatus.collector_attached;
  const collectorFault = ["session_lost", "stalled"].includes(collectorState);
  const collectorReady = collectorAttached && ["listening", "idle", "receiving"].includes(collectorState) && !collectorFault;
  const analysisReady = !!wechatCollectorStatus.analysis_running;
  const accountBlocked = !!stageOsAccount.configured && !stageOsAccount.logged_in;
  const accountEntitlementBlocked = !!stageOsAccount.configured && !!stageOsAccount.logged_in && !stageOsAccount.managed_ai_ready;
  const profileReady = !!state.profile?.has_profile;
  const overlayReady = !!stageOsOverlayVisible;
  const requiredReady = collectorReady && analysisReady;
  const requiredCount = Number(collectorReady) + Number(analysisReady);
  const lastCheck = stageOsPreflightAge(wechatCollectorStatus.updated_at_ms);
  const task = !collectorReady ? "collector" : !analysisReady ? "analysis" : "ready";
  return {
    collectorState,
    collectorAttached,
    collectorFault,
    collectorReady,
    analysisReady,
    accountBlocked,
    accountEntitlementBlocked,
    profileReady,
    overlayReady,
    requiredReady,
    requiredCount,
    lastCheck,
    task
  };
}

function stageOsPreflightSteps(preflight = stageOsPreflightState()) {
  const collectorStatus = preflight.collectorReady
    ? "已连接"
    : preflight.collectorFault
      ? "连接中断"
      : preflight.collectorState === "starting"
        ? "连接中"
        : "未连接";
  return [
    {
      id: "profile",
      index: "01",
      label: "主播档案",
      detail: preflight.profileReady ? "语气偏好已保存" : "建议完善，提醒会更像你",
      status: preflight.profileReady ? "已完成" : "建议",
      tone: preflight.profileReady ? "ready" : "optional",
      route: "profile"
    },
    {
      id: "collector",
      index: "02",
      label: "直播数据",
      detail: preflight.collectorReady ? "正在接收真实直播间事件" : preflight.collectorFault ? "需要重新确认登录与连接" : "连接直播伴侣后再开播",
      status: collectorStatus,
      tone: preflight.collectorReady ? "ready" : preflight.collectorFault ? "error" : "pending",
      route: "capture"
    },
    {
      id: "analysis",
      index: "03",
      label: "AI 分析服务",
      detail: preflight.analysisReady
        ? "本地分析引擎运行中"
        : preflight.accountBlocked
          ? "一次性激活后启用托管 AI"
          : preflight.accountEntitlementBlocked
            ? "账号权益已到期、暂停或尚未开通"
            : firstText(wechatCollectorStatus.analysis_status) || "正在检测本地分析引擎",
      status: preflight.analysisReady ? "运行中" : preflight.accountBlocked ? "待激活" : preflight.accountEntitlementBlocked ? "待开通" : "未就绪",
      tone: preflight.analysisReady ? "ready" : "pending",
      route: preflight.accountBlocked || preflight.accountEntitlementBlocked ? "account" : "diagnostics"
    },
    {
      id: "overlay",
      index: "04",
      label: "提示悬浮窗",
      detail: preflight.overlayReady ? "提醒会显示在直播画面旁" : "可选，开播后也能开启",
      status: preflight.overlayReady ? "已开启" : "可选",
      tone: preflight.overlayReady ? "ready" : "optional",
      route: "overlay"
    }
  ];
}

function stageOsPreflightRail() {
  const preflight = stageOsPreflightState();
  const steps = stageOsPreflightSteps(preflight);
  return `
    <div class="os-preflight-rail-list" aria-label="开播前四步检查">
      ${steps.map((step) => `
        <button class="os-preflight-rail-step ${step.tone} ${preflight.task === step.id ? "current" : ""}"
                data-stage-nav="${step.route}">
          <span class="os-preflight-step-index">${step.tone === "ready" ? '<i class="os-fluent">&#xE73E;</i>' : step.index}</span>
          <span><b>${step.label}</b><small>${escapeHtml(step.detail)}</small></span>
          <em>${step.status}</em>
        </button>
      `).join("")}
    </div>
  `;
}

function stageOsPreflightSidebarFooter() {
  const preflight = stageOsPreflightState();
  return `
    <div class="os-preflight-side-state ${preflight.requiredReady ? "ready" : "waiting"}">
      <span></span><b>必要项 ${preflight.requiredCount}/2</b><small>${preflight.requiredReady ? "可以开始" : "仍需检查"}</small>
    </div>
    <p class="os-preflight-side-note">软件不会替你开启直播。所有状态确认后，仍由你亲自开始计时与提醒。</p>
    ${pageNotice?.message ? `<p class="os-sidebar-notice warning"><span>${escapeHtml(pageNotice.message)}</span><button data-dismiss-notice aria-label="关闭提示">×</button></p>` : ""}
  `;
}

function stageOsPreflightTask(preflight) {
  if (preflight.task === "collector") {
    if (preflight.collectorFault) {
      return {
        eyebrow: "需要处理 · 直播数据",
        title: "直播数据连接中断",
        summary: "AI 教练已停止读取直播间事件，不会使用过期数据继续判断。",
        status: firstText(wechatCollectorStatus.collector_state_label) || "连接已失效",
        detail: "常见原因是直播伴侣登录失效、窗口被关闭，或采集进程暂时中断。",
        tone: "error"
      };
    }
    if (preflight.collectorState === "starting") {
      return {
        eyebrow: "正在处理 · 直播数据",
        title: "正在连接直播数据",
        summary: "保持直播伴侣开启，连接完成后这里会自动更新。",
        status: firstText(wechatCollectorStatus.collector_state_label) || "正在连接",
        detail: "连接期间不会生成基于直播间数据的提醒。",
        tone: "progress"
      };
    }
    return {
      eyebrow: "下一步 · 直播数据",
      title: "先连接真实直播数据",
      summary: "AI 教练需要评论、礼物和在线变化作为判断依据。未连接时不会猜测。",
      status: firstText(wechatCollectorStatus.collector_state_label) || "尚未启动采集",
      detail: "点击开启后，只读取本场直播所需的事件；遇到登录问题可进入数据采集页检查。",
      tone: "pending"
    };
  }
  if (preflight.task === "analysis") {
    return {
      eyebrow: "下一步 · AI 分析服务",
      title: preflight.accountBlocked ? "先激活托管 AI 账号" : preflight.accountEntitlementBlocked ? "账号权益暂不可用" : "等待 AI 分析服务就绪",
      summary: preflight.accountBlocked
        ? "直播数据已经连通；完成一次性激活后，豆包 Key 和模型费用由产品方服务承担。"
        : preflight.accountEntitlementBlocked
          ? "账号已登录，但权益已到期、暂停或尚未开通；不会静默改用主播个人 Key。"
          : "直播数据已经连通，但本地分析引擎还没有进入运行状态。",
      status: preflight.accountBlocked ? "账号待激活" : preflight.accountEntitlementBlocked ? "账号权益不可用" : firstText(wechatCollectorStatus.analysis_status) || "正在检测本地分析引擎",
      detail: preflight.accountBlocked || preflight.accountEntitlementBlocked
        ? "打开账号与会员页查看当前状态；未授权时仍可进入本地低风险模式。"
        : "可以重新检测；若仍未就绪，进入诊断页查看本地组件和账号权益。",
      tone: "pending"
    };
  }
  return {
    eyebrow: "检查完成 · 由你开始",
    title: "开播前必要项已就绪",
    summary: "真实直播数据和 AI 分析服务都已准备好。软件不会自动开始计时。",
    status: "可以开启直播教练",
    detail: "档案与悬浮窗属于建议项，可以现在完善，也可以开播后调整。",
    tone: "ready"
  };
}

function stageOsPreflightPrimaryActions(preflight) {
  if (preflight.task === "collector") {
    if (preflight.collectorFault) {
      return `
        <button class="os-preflight-primary" data-stage-nav="capture"><i class="os-fluent">&#xE774;</i>检查登录与连接</button>
        <button class="os-preflight-secondary" data-preflight-recheck>重新检测</button>
      `;
    }
    if (preflight.collectorState === "starting") {
      return `
        <button class="os-preflight-primary" data-preflight-recheck><i class="os-fluent">&#xE895;</i>刷新连接状态</button>
        <button class="os-preflight-secondary" data-stage-nav="capture">查看采集详情</button>
      `;
    }
    return `
      <button class="os-preflight-primary" data-toggle-live-data><i class="os-fluent">&#xE768;</i>开启直播数据</button>
      <button class="os-preflight-secondary" data-stage-nav="capture">查看连接说明</button>
    `;
  }
  if (preflight.task === "analysis") {
    if (preflight.accountBlocked || preflight.accountEntitlementBlocked) {
      return `
        <button class="os-preflight-primary" data-stage-nav="account"><i class="os-fluent">&#xE77B;</i>打开账号与会员</button>
        <button class="os-preflight-secondary" data-preflight-recheck>重新检测</button>
      `;
    }
    return `
      <button class="os-preflight-primary" data-preflight-recheck><i class="os-fluent">&#xE895;</i>重新检测服务</button>
      <button class="os-preflight-secondary" data-stage-nav="diagnostics">打开诊断</button>
    `;
  }
  return `
    <button class="os-preflight-primary" data-preflight-recheck><i class="os-fluent">&#xE895;</i>再次检查状态</button>
    <button class="os-preflight-secondary" data-stage-nav="profile">检查主播档案</button>
  `;
}

function stageOsPreflightSummaryRow(step) {
  const actionAttr = step.id === "collector"
    ? (step.tone === "error" ? 'data-stage-nav="capture"' : "data-toggle-live-data")
    : step.id === "analysis"
      ? "data-preflight-recheck"
      : step.id === "overlay"
        ? "data-toggle-overlay"
        : 'data-stage-nav="profile"';
  return `
    <button class="os-preflight-summary-row ${step.tone}" ${actionAttr}>
      <i class="os-fluent">${step.tone === "ready" ? "&#xE73E;" : step.tone === "error" ? "&#xEA39;" : "&#xE946;"}</i>
      <span><b>${step.label}</b><small>${escapeHtml(step.detail)}</small></span>
      <em>${step.status}</em>
    </button>
  `;
}

function stageOsPreflightAudio() {
  const audioMode = smartRackStatus?.mode || "none";
  const voltuneOnline = !!smartRackStatus?.voltune?.connected;
  const soundOnline = !!smartRackStatus?.soundstage?.connected;
  const enabled = audioMode !== "none";
  const status = enabled
    ? (voltuneOnline || soundOnline ? "已连接音频助手" : "已选择，等待连接")
    : "未启用，不影响 AI 教练";
  return `
    <section class="os-preflight-audio ${stageOsPreflightAudioOpen ? "open" : ""}">
      <div class="os-preflight-audio-head">
        <i class="os-fluent">&#xE995;</i>
        <span><b>音频能力</b><small>${status}</small></span>
        <em>可选</em>
        <button data-toggle-preflight-audio aria-expanded="${stageOsPreflightAudioOpen}">${stageOsPreflightAudioOpen ? "收起" : "展开"}<i class="os-fluent">&#xE70D;</i></button>
      </div>
      ${stageOsPreflightAudioOpen ? `
        <div class="os-preflight-audio-detail">
          <span><i class="${voltuneOnline ? "online" : ""}"></i>VOLTUNE ${voltuneOnline ? "已连接" : "未连接"}</span>
          <span><i class="${soundOnline ? "online" : ""}"></i>SoundStage ${soundOnline ? "已连接" : "未连接"}</span>
          <p>音效和电音属于直播辅助能力，不会影响数据采集与 AI 判断。</p>
          <button data-open-audio>进入完整音频工作台 <i class="os-fluent">&#xE72A;</i></button>
        </div>
      ` : ""}
    </section>
  `;
}

function stageOsPreflightPage() {
  const preflight = stageOsPreflightState();
  const task = stageOsPreflightTask(preflight);
  const steps = stageOsPreflightSteps(preflight);
  const progress = Math.round((preflight.requiredCount / 2) * 100);
  return `
    <div class="os-page os-preflight-page">
      <div class="os-preflight-shell">
        <main class="os-preflight-focus ${task.tone}">
          <header>
            <span>${task.eyebrow}</span>
            <small>最近检查：${escapeHtml(preflight.lastCheck)}</small>
          </header>
          <div class="os-preflight-focus-copy">
            <div class="os-preflight-state-mark"><i class="os-fluent">${task.tone === "ready" ? "&#xE73E;" : task.tone === "error" ? "&#xEA39;" : "&#xE946;"}</i></div>
            <h1>${task.title}</h1>
            <p>${task.summary}</p>
          </div>
          <section class="os-preflight-diagnosis" aria-live="polite">
            <div><span></span><b>${escapeHtml(task.status)}</b><em>${preflight.task === "collector" ? "直播数据" : preflight.task === "analysis" ? "分析服务" : "开播状态"}</em></div>
            <p>${task.detail}</p>
          </section>
          <div class="os-preflight-required-progress">
            <div><span>必要项进度</span><b>${preflight.requiredCount}/2</b></div>
            <i><em style="width:${progress}%"></em></i>
          </div>
          <div class="os-preflight-focus-actions">${stageOsPreflightPrimaryActions(preflight)}</div>
          <footer><i class="os-fluent">&#xE946;</i><span>进入软件不会自动开播；只有你点击后，教练才开始计时与提醒。</span></footer>
        </main>
        <aside class="os-preflight-summary">
          <div class="os-preflight-coach">
            <img src="${stageOsCoachAsset}" alt="AI 直播教练形象" />
            <div><span>LIVECOACH AI</span><b>${preflight.requiredReady ? "必要项已经就绪" : "我会陪你逐项检查"}</b><small>${preflight.requiredReady ? "是否开播仍由你决定" : "先处理红色或黄色状态"}</small></div>
          </div>
          <div class="os-preflight-summary-list">${steps.map(stageOsPreflightSummaryRow).join("")}</div>
          <div class="os-preflight-start-block">
            <button class="os-preflight-start" data-start-session ${preflight.requiredReady ? "" : "disabled"}>
              <i class="os-fluent">&#xE768;</i><span>开启直播教练<small>${preflight.requiredReady ? "由你手动开始计时与提醒" : "先完成直播数据与分析服务"}</small></span>
            </button>
            ${preflight.requiredReady ? "" : `
              <button class="os-preflight-local" data-start-local-session>进入本地低风险模式 <i class="os-fluent">&#xE72A;</i></button>
              <p>不会伪造直播数据；缺少证据时不生成具体提醒。</p>
            `}
          </div>
        </aside>
        ${stageOsPreflightAudio()}
      </div>
    </div>
  `;
}

function stageOsLivePage() {
  const isLive = stageOsSessionPhase === "live";
  if (!isLive) return stageOsPreflightPage();
  const snapshot = latest?.snapshot || {};
  const captureActive = !!wechatCollectorStatus.collector_attached || !!wechatCollectorStatus.media_capture_running;
  const hasReminderEvidence = !!firstText(snapshot.teleprompter_line, snapshot.advice);
  const advice = isLive
    ? firstText(snapshot.teleprompter_line, snapshot.advice) ||
      (captureActive ? "AI 已就绪，等待真实直播事件。" : "采集尚未开启，暂不生成提醒。")
    : "准备好后，由你亲自开启直播教练。";
  const diagnosis = isLive
    ? firstText(snapshot.diagnosis, snapshot.live_room_diagnosis) ||
      (captureActive ? "暂无关键事件" : "等待直播数据或音画采集")
    : "直播现场 · 等待开播";
  const direction = isLive
    ? firstText(snapshot.why_it_matters) ||
      (captureActive ? "当前没有足够证据，不生成具体话术" : "请先开启下方任一采集开关")
    : "进入应用不会自动计时，也不会自动显示正在直播";
  const nextMove = isLive
    ? firstText(snapshot.next_move) ||
      (captureActive ? "正常直播即可，有真实弹幕或礼物时再提醒" : "看到「监听中」或「运行中」后再正常直播")
    : "先确认下方采集状态，再点击「开启直播」";
  const { viewers, comments, gifts } = stageOsLiveMetrics(snapshot);
  const liveLabel = isLive
    ? latest?.connected === false
      ? (latest.status || "等待建议连接")
      : hasReminderEvidence || captureActive ? "AI 已就绪" : "等待采集"
    : "尚未开始";
  const relationship = isLive
    ? firstText(snapshot.relationship_stage, snapshot.relationship_level) || "暂无关系判断"
    : state.profile?.has_profile ? "档案已填写" : "主播档案待填写";
  const tone = isLive
    ? (nextMove.includes("｜") ? nextMove.split("｜")[0] : firstText(snapshot.tone, snapshot.suggested_tone)) || "保持自然语气"
    : "开播前可随时调整";
  const sceneLabel = firstText(wechatCollectorStatus.media_scene_label);
  const sceneConfidence = Number(wechatCollectorStatus.media_scene_confidence || 0);
  const sceneEvent = wechatCollectorStatus.media_scene_fresh && sceneLabel
    ? `画面已识别 · ${sceneLabel}${sceneConfidence > 0 ? ` · ${Math.round(sceneConfidence * 100)}%` : ""}`
    : firstText(wechatCollectorStatus.media_capture_detail);
  const collectorEvent = sceneEvent ||
    wechatCollectorStatus.collector_state_label ||
    "尚未启动采集";
  const recentComments = Array.isArray(wechatCollectorStatus.recent_comments)
    ? wechatCollectorStatus.recent_comments
    : [];
  const latestComment = recentComments[0] || null;
  const latestCommentBody = firstText(latestComment?.content);
  const latestCommentUser = firstText(latestComment?.user_name);
  const commentEvent = latestCommentBody
    ? `最新评论 · ${latestCommentUser ? `${latestCommentUser}：` : ""}${latestCommentBody}`
    : comments > 0
      ? `本场已接收 ${comments} 条评论`
      : "等待新的观众评论";
  const commentEventTime = latestCommentBody
    ? stageOsCommentClock(latestComment?.timestamp_ms)
    : "评论";
  const giftEvent = gifts > 0 ? `本场已记录 ${gifts} 次礼物事件` : "礼物事件尚未触发";
  return `
    <div class="os-page os-live-page">
      ${isLive ? stageOsLiveHeaderStatus() : ""}
      <section class="os-live-stage">
        <div class="os-live-copy">
          <div class="os-kicker os-reminder-reason">
            <small class="os-field-label">你现在的直播间</small>
            <span>${escapeHtml(diagnosis)}</span>
          </div>
          <div class="os-advice-label">现在最值得做</div>
          <h1>${escapeHtml(advice)}</h1>
          <div class="os-speech-card">
            <i class="os-fluent">&#xE945;</i>
            <div>
              <small class="os-field-label">这样做可能得到什么</small>
              <b>${escapeHtml(direction)}</b>
              <small class="os-field-label os-field-label-cue">何时做 · 怎么做 · 用什么状态</small>
              <span>${escapeHtml(nextMove)}</span>
            </div>
          </div>
        </div>
        <div class="os-live-coach-hero">
          <img src="${stageOsCoachHeroAsset}" alt="正在向主播给出建议的 AI 教练" />
        </div>
        <div class="os-live-timeline">
          <div class="os-flow-head"><span>${isLive ? "实时节奏" : "开播状态"}</span><b>${escapeHtml(liveLabel)}</b></div>
          ${stageOsEventItem(isLive ? "现在" : "待机", advice, "active")}
          ${stageOsEventItem("采集", collectorEvent, "mint")}
          ${stageOsEventItem(commentEventTime, commentEvent, "violet")}
          ${stageOsEventItem("礼物", giftEvent, "coral")}
        </div>
        <aside class="os-relation-panel">
          <div class="os-relation-level"><i class="os-fluent">&#xEB51;</i><b>${escapeHtml(relationship)}</b><span class="os-fluent">&#xE72A;</span></div>
          <h3>${isLive ? "为什么这一步值得做" : "档案决定提醒语气"}</h3>
          <p>${escapeHtml(direction)}</p>
          <div class="os-tone-block">
            <b>建议语气 · ${escapeHtml(tone)}</b>
            <div class="os-tone-track"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
          </div>
          <div class="os-next-actions">
              <h3>${isLive ? "这一步的情绪与距离" : "开播前"}</h3>
              <button><span>${escapeHtml(nextMove)}</span><i class="os-fluent">&#xE72A;</i></button>
              <button data-stage-nav="${isLive ? "live" : "profile"}"><span>${isLive ? "只推进当前一步，有反馈后再决定下一步" : "填写主播档案，让建议更像你"}</span><i class="os-fluent">&#xE72A;</i></button>
            </div>
          </aside>
        </section>
      ${stageOsControlIsland()}
      ${stageOsAudioDeck(viewers, comments, gifts)}
      ${stageOsEndModal()}
      ${stageOsBigBoardDrawer()}
    </div>
  `;
}

function stageOsBigBoardDrawer() {
  if (!stageOsBigBoard.drawerOpen) return "";
  const report = stageOsBigBoard.report || {};
  const status = stageOsBigBoard.status || "idle";
  const cutoff = stageOsBigBoard.dataCutoffMs
    ? stageOsFormatDateTime(stageOsBigBoard.dataCutoffMs)
    : "点击时刻";
  const generated = stageOsBigBoard.generatedAtMs
    ? stageOsFormatDateTime(stageOsBigBoard.generatedAtMs)
    : "生成中";
  const content = status === "loading"
    ? stageOsBigBoardLoading()
    : status === "error"
      ? stageOsBigBoardError()
      : status === "ready" && stageOsBigBoard.report
        ? stageOsBigBoardReport(report)
        : stageOsBigBoardEmpty();
  return `
    <div class="os-big-board-scrim" data-big-board-close></div>
    <aside class="os-big-board-drawer" role="dialog" aria-modal="true" aria-label="当前直播间大盘报告">
      <header class="os-big-board-head">
        <div>
          <span>MANUAL LIVE REPORT</span>
          <h2>当前直播间大盘报告</h2>
          <p>数据截止 ${escapeHtml(cutoff)}　·　报告生成 ${escapeHtml(generated)}</p>
        </div>
        <button data-big-board-close aria-label="关闭大盘报告">×</button>
      </header>
      ${stageOsBigBoard.newReminderBanner ? `
        <button class="os-big-board-reminder" data-big-board-reminder>
          <i></i><span><b>刚收到一条新的实时提醒</b><small>实时提醒优先，点击回到直播现场查看</small></span><em>查看</em>
        </button>
      ` : ""}
      <div class="os-big-board-scroll">${content}</div>
    </aside>
  `;
}

function stageOsBigBoardLoading() {
  return `
    <div class="os-big-board-loading" aria-live="polite">
      <div class="os-big-board-orbit"><img src="${stageOsCoachAsset}" alt="" /></div>
      <h3>AI 正在读取当下数据变化</h3>
      <p>会把当前截面、最近变化和上一份结论边界一起核对。通常需要十几秒，实时提醒仍会继续。</p>
      <div class="os-big-board-progress"><i></i></div>
      <div class="os-big-board-skeleton"><span></span><span></span><span></span><span></span></div>
    </div>
  `;
}

function stageOsBigBoardError() {
  return `
    <div class="os-big-board-error-state">
      <i class="os-fluent">&#xEA39;</i>
      <h3>本次大盘报告没有生成</h3>
      <p>${escapeHtml(stageOsBigBoard.error || "本地分析或模型连接暂时不可用，实时提醒不受影响。")}</p>
      <button data-big-board-regenerate>重新分析当下</button>
    </div>
  `;
}

function stageOsBigBoardEmpty() {
  return `
    <div class="os-big-board-error-state">
      <h3>还没有本场大盘报告</h3>
      <p>继续直播并保持数据采集；达到可分析条件后，顶部按钮会变为可用。</p>
    </div>
  `;
}

function stageOsBigBoardReport(report) {
  const evidence = Array.isArray(report.evidence) ? report.evidence : [];
  const uncertainties = Array.isArray(report.uncertainties) ? report.uncertainties : [];
  return `
    <section class="os-big-board-lead">
      <span>一、当前直播间核心现状判断</span>
      <h3>${escapeHtml(report.overall_judgment || "当前证据不足，先保持直播并继续采集。")}</h3>
    </section>
    <div class="os-big-board-dimensions">
      ${stageOsBigBoardDimension("01", "流量端", report.traffic_status)}
      ${stageOsBigBoardDimension("02", "互动端", report.interaction_status)}
      ${stageOsBigBoardDimension("03", "转化与商业支持端", report.commercial_status)}
      ${stageOsBigBoardDimension("04", "内容端", report.content_status)}
    </div>
    <section class="os-big-board-actions">
      <span>二、分优先级优化建议</span>
      ${stageOsBigBoardAction("现在立刻做", report.immediate_action, "primary")}
      ${stageOsBigBoardAction("接下来 10 分钟", report.ten_minute_experiment, "experiment")}
      ${stageOsBigBoardAction("当前不要做", report.do_not_do, "avoid")}
    </section>
    <details class="os-big-board-evidence">
      <summary><span>三、判断依据与不确定项</span><small>展开查看事实边界</small></summary>
      <div>
        <h4>本次判断依据</h4>
        <ul>${evidence.length ? evidence.map((item) => `<li>${escapeHtml(item)}</li>`).join("") : "<li>当前未返回可复核依据。</li>"}</ul>
        <h4>不确定项 / 待确认</h4>
        <ul>${uncertainties.length ? uncertainties.map((item) => `<li>${escapeHtml(item)}</li>`).join("") : "<li>无额外待确认项。</li>"}</ul>
      </div>
    </details>
    <footer class="os-big-board-footer">
      <div><b>${escapeHtml(stageOsBigBoard.model || "Doubao Pro")}</b><span>${stageOsBigBoard.latencyMs ? `${(stageOsBigBoard.latencyMs / 1000).toFixed(1)} 秒` : "本次报告"}</span></div>
      <button data-big-board-regenerate>重新分析当下</button>
    </footer>
  `;
}

function stageOsBigBoardDimension(index, title, body) {
  return `
    <article><b>${index}</b><div><span>${escapeHtml(title)}</span><p>${escapeHtml(body || "当前暂无足够证据。")}</p></div></article>
  `;
}

function stageOsBigBoardAction(label, body, tone) {
  return `
    <article class="${tone}"><span>${escapeHtml(label)}</span><p>${escapeHtml(body || "暂无建议。")}</p></article>
  `;
}

function stageOsFormatDateTime(timestampMs) {
  const date = new Date(Number(timestampMs || 0));
  if (Number.isNaN(date.getTime())) return "—";
  return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
}

function stageOsEventItem(time, text, tone) {
  return `<div class="os-flow-item ${tone}"><time>${time}</time><span></span><p>${escapeHtml(text)}</p></div>`;
}

function stageOsControlIsland() {
  const isLive = stageOsSessionPhase === "live";
  const collectorState = wechatCollectorStatus.collector_state || "not_started";
  const collectorActive = !!wechatCollectorStatus.collector_attached;
  const collectorSuccess = collectorState === "receiving";
  const collectorFault = ["session_lost", "stalled"].includes(collectorState);
  const collectorStatus = collectorState === "receiving"
    ? "采集成功 · 正在接收"
    : collectorState === "idle"
      ? "连接正常 · 暂无新数据"
      : collectorState === "listening"
        ? "监听成功 · 等待直播数据"
        : collectorState === "starting"
          ? "正在连接采集接口"
          : collectorFault
            ? "数据已停止 · 点击检查登录"
            : "未开启 · 点击开始采集";
  const mediaRequested = !!wechatCollectorStatus.media_capture_requested;
  const mediaRunning = !!wechatCollectorStatus.media_capture_running;
  const mediaSuccess = !!wechatCollectorStatus.media_capture_success;
  const mediaStatus = mediaRunning
    ? "采集成功 · 音画正在送入 AI"
    : mediaRequested
      ? wechatCollectorStatus.media_capture_status || "启动失败，请检查本机组件"
      : "未开启 · 不会采集本机音画";
  const overlayControl = `
    <button class="os-compact-control os-overlay-toggle ${stageOsOverlayVisible ? "active" : ""}" data-toggle-overlay aria-pressed="${stageOsOverlayVisible}">
      <i class="os-control-icon os-fluent">&#xE7C4;</i>
      <span>悬浮窗<small>${stageOsOverlayVisible ? "已开启" : "已关闭"}</small></span>
      <em></em>
    </button>
  `;
  return `
    <div class="os-control-island ${isLive ? "live" : "prelive"}">
      <button class="os-capture-control ${collectorSuccess ? "ready" : collectorFault ? "error" : collectorActive ? "warning" : ""}" data-toggle-live-data aria-pressed="${collectorActive}">
        <i class="os-control-icon os-fluent">&#xE9D9;</i>
        <span><b>直播数据</b><small>${escapeHtml(wechatCollectorStatus.collector_state_label || collectorStatus)}</small></span>
        <em><i></i>${collectorFault ? "失效" : collectorActive ? "已开" : "开启"}</em>
      </button>
      <label class="os-capture-control os-media-toggle ${mediaSuccess ? "ready" : mediaRequested ? "warning" : ""}">
        <i class="os-control-icon os-fluent">&#xE714;</i>
        <span><b>音画采集</b><small>${escapeHtml(wechatCollectorStatus.media_capture_detail || mediaStatus)}</small></span>
        <input type="checkbox" data-media-capture-toggle ${mediaRequested ? "checked" : ""} />
        <i class="os-toggle-track"><b></b></i>
      </label>
      ${overlayControl}
      ${isLive ? `
        <button class="os-compact-control" data-pause-reminders><i class="os-control-icon os-fluent">${stageOsRemindersPaused ? "&#xE768;" : "&#xE769;"}</i><span>${stageOsRemindersPaused ? "继续提醒" : "暂停提醒"}</span></button>
        <button class="danger" data-end-session><i class="os-control-icon os-fluent">&#xE71A;</i><span>结束直播<small>结束后生成本场承接</small></span></button>
      ` : `
        <button class="start-live" data-start-session><i class="os-control-icon os-fluent">&#xE768;</i><span>开启直播<small>由主播手动开始计时与提醒</small></span></button>
      `}
    </div>
  `;
}

function stageOsAudioDeck(viewers, comments, gifts) {
  const voltuneOnline = !!smartRackStatus?.voltune?.connected;
  const soundOnline = !!smartRackStatus?.soundstage?.connected;
  const audioMode = smartRackStatus?.mode || "none";
  const audioStatus = audioMode === "none"
    ? "未启用（可选，不影响 AI）"
    : voltuneOnline || soundOnline
      ? "已按主播选择连接"
      : "已选择，等待配置";
  const microphone = smartRackStatus?.audio_devices?.microphone || {};
  const output = smartRackStatus?.audio_devices?.output || {};
  return `
    <section class="os-audio-deck">
      <div class="os-deck-head">
        <div><i class="os-fluent">&#xE995;</i><span>音频控制面板</span><b>${audioStatus}</b></div>
        <button data-open-audio>进入完整音频工作台 <i class="os-fluent">&#xE72A;</i></button>
      </div>
      <div class="os-deck-grid">
        <article class="os-voice-console">
          <div class="os-mini-heading"><span><i class="os-fluent">&#xE720;</i> VOLTUNE 电音</span><b class="${voltuneOnline ? "online" : ""}">${voltuneOnline ? "处理正常" : "等待连接"}</b></div>
          <div class="os-wave-monitor">
            <div class="os-wave-labels"><span>0dB<br><small>-12dB<br>-24dB<br>-48dB</small></span><time>00:00　　　　00:10　　　　00:20</time></div>
            <canvas data-wave-src="assets/stage-os/waveforms/applause.ogg" aria-label="预设试听波形"></canvas>
            <div class="os-level-meters"><i></i><i></i><small>L　R</small></div>
          </div>
          <div class="os-preset-row">
            <button class="selected" data-audio-preset="原声" ${voltuneOnline ? "" : "disabled"}>原声</button>
            <button data-audio-preset="霓虹电音" ${voltuneOnline ? "" : "disabled"}>霓虹电音</button>
            <button data-audio-preset="未来机甲" ${voltuneOnline ? "" : "disabled"}>未来机甲</button>
            <button data-audio-preset="微醺氛围" ${voltuneOnline ? "" : "disabled"}>微醺氛围</button>
          </div>
          <div class="os-voice-controls">
            ${stageOsRange("处理强度", "electro", stageOsAudioValues.electro, !voltuneOnline)}
            ${stageOsRange("音高", "pitch", stageOsAudioValues.pitch, !voltuneOnline)}
            <div class="os-timbre-control"><span>音色塑形</span><b>柔和　—　明亮</b><div><i></i></div></div>
          </div>
          <div class="os-device-mini"><span>输入　${escapeHtml(microphone.detail || "麦克风")}</span><b>${microphone.detected ? "已识别" : "未检测到"}</b><span>输出　${escapeHtml(output.detail || "系统输出")}</span><b>${output.detected ? "已识别" : "未检测到"}</b></div>
        </article>
        <article class="os-sound-console">
          <div class="os-mini-heading">
            <span><i class="os-fluent">&#xE945;</i> SoundStage 音效</span>
            <label class="os-inline-range" style="--value:${stageOsAudioValues.masterVolume}%">主音量 <b>${Math.round(stageOsAudioValues.masterVolume)}%</b><input type="range" min="0" max="100" value="${stageOsAudioValues.masterVolume}" data-audio-range="masterVolume" ${soundOnline ? "" : "disabled"} /></label>
          </div>
          <div class="os-sound-grid">
            ${stageOsWavePad("闪亮登场", "开场", "01", "&#xE734;", "celebration.ogg", !soundOnline)}
            ${stageOsWavePad("全场掌声", "互动", "02", "&#xE8FB;", "applause.ogg", !soundOnline)}
            ${stageOsWavePad("答对啦", "回应", "03", "&#xE73E;", "correct.ogg", !soundOnline)}
            ${stageOsWavePad("感谢礼物", "礼物", "04", "&#xE7EF;", "gift-chime.ogg", !soundOnline)}
            ${stageOsWavePad("快速转场", "转场", "05", "&#xE8FD;", "reveal.ogg", !soundOnline)}
            ${stageOsWavePad("悬念鼓点", "气氛", "06", "&#xEA38;", "drum-roll.ogg", !soundOnline)}
          </div>
        </article>
      </div>
      <div class="os-audio-route">
        <span><i class="os-fluent">&#xE720;</i><b>麦克风</b><small>本机输入</small></span>
        <i class="os-fluent">&#xE72A;</i>
        <span><i class="os-fluent">&#xE9D9;</i><b>VOLTUNE</b><small>电音处理</small></span>
        <i class="os-fluent">&#xE72A;</i>
        <span><i class="os-fluent">&#xE7F5;</i><b>直播输出</b><small>系统默认输出</small></span>
        <strong class="${audioMode === "none" ? "optional" : voltuneOnline || soundOnline ? "ready" : ""}"><i class="os-fluent">&#xE73E;</i>${audioMode === "none" ? "未启用助手，不影响 AI" : voltuneOnline || soundOnline ? "音效输出正常" : "等待完成配置"}</strong>
        <button data-open-audio><i class="os-fluent">&#xE713;</i>展开完整音频工作台<i class="os-fluent">&#xE72A;</i></button>
      </div>
    </section>
  `;
}

function stageOsWavePad(name, category, number, icon, file, disabled = false) {
  return `
    <button data-sound-pad="${name}" ${disabled ? "disabled" : ""}>
      <span class="os-pad-head"><small>${number}</small><i class="os-fluent">${icon}</i><b>${name}</b></span>
      <canvas data-wave-src="assets/stage-os/waveforms/${file}" aria-label="${name}音效波形"></canvas>
      <em>${category}</em>
    </button>
  `;
}

async function drawStageOsWaveforms() {
  const canvases = Array.from(document.querySelectorAll("canvas[data-wave-src]"));
  if (!canvases.length) return;
  const context = window.__stageOsAudioContext || new AudioContext();
  window.__stageOsAudioContext = context;
  window.__stageOsWaveCache ||= new Map();
  await Promise.all(canvases.map(async (canvas) => {
    try {
      if (!window.__stageOsWaveCache.has(canvas.dataset.waveSrc)) {
        window.__stageOsWaveCache.set(canvas.dataset.waveSrc, (async () => {
          const response = await fetch(canvas.dataset.waveSrc);
          const buffer = await response.arrayBuffer();
          return context.decodeAudioData(buffer.slice(0));
        })());
      }
      const audio = await window.__stageOsWaveCache.get(canvas.dataset.waveSrc);
      const samples = audio.getChannelData(0);
      const box = canvas.getBoundingClientRect();
      const ratio = window.devicePixelRatio || 1;
      canvas.width = Math.max(2, Math.round(box.width * ratio));
      canvas.height = Math.max(2, Math.round(box.height * ratio));
      const drawing = canvas.getContext("2d");
      drawing.clearRect(0, 0, canvas.width, canvas.height);
      drawing.strokeStyle = canvas.closest(".os-wave-monitor") ? "#2fd0b3" : "#75cfc1";
      drawing.lineWidth = Math.max(1, ratio);
      drawing.beginPath();
      const center = canvas.height / 2;
      const step = Math.max(1, Math.floor(samples.length / canvas.width));
      for (let x = 0; x < canvas.width; x += 1) {
        let peak = 0;
        const start = x * step;
        for (let i = start; i < Math.min(start + step, samples.length); i += 1) {
          peak = Math.max(peak, Math.abs(samples[i]));
        }
        const height = Math.max(1, peak * canvas.height * 0.47);
        drawing.moveTo(x + 0.5, center - height);
        drawing.lineTo(x + 0.5, center + height);
      }
      drawing.stroke();
    } catch {
      canvas.classList.add("wave-unavailable");
    }
  }));
}

function stageOsAiSongPage() {
  const installed = !!aiSongStatus?.installed;
  const running = !!aiSongStatus?.running;
  const memoryReady = !!aiSongStatus?.memory_ready;
  const availableMemory = Number(aiSongStatus?.available_memory_gb || 0);
  const minimumMemory = Number(aiSongStatus?.minimum_memory_gb || 4);
  const worker = aiSongStatus?.worker || {};
  const engine = worker.engine || {};
  const recording = worker.recording || {};
  const jobRunning = worker.pipeline_state === "running";
  const liveBlocked = stageOsSessionPhase === "live" && !running;
  const canOpen = installed && (running || (memoryReady && !liveBlocked));
  const statusTone = running ? "running" : installed ? "ready" : "missing";
  const statusLabel = running ? "无头后台已连接" : installed ? "后台组件可按需启动" : "未检测到后台组件";
  const memoryLabel = availableMemory > 0 ? `${availableMemory.toFixed(1)} GB` : "检测中";
  const primaryLabel = running ? "后台已连接" : liveBlocked ? "直播中暂不可启动" : "启动 AI 做歌后台";

  if (stageOsCurrentRoute === "song-tasks") {
    const logs = Array.isArray(worker.logs) ? worker.logs : [];
    return `<main class="os-ai-song-page os-ai-song-task-page">
      <div class="os-workspace-title"><div><div class="os-kicker">创作任务</div><h1>后台处理进度</h1><p>分离、音色转换和混音保持串行，完成或取消后释放子进程资源。</p></div><button data-refresh-ai-song>刷新</button></div>
      <section class="os-song-task-card ${escapeHtml(worker.pipeline_state || "idle")}">
        <header><div><span>${escapeHtml(worker.pipeline_state || "idle")}</span><h2>${escapeHtml(worker.stage || "等待任务")}</h2><p>${escapeHtml(worker.message || "尚未提交歌曲任务")}</p></div><b>${Number(worker.percent || 0)}%</b></header>
        <div class="os-song-progress"><i style="width:${Math.max(0, Math.min(100, Number(worker.percent || 0)))}%"></i></div>
        <div class="os-song-task-actions"><button data-stage-nav="song">新建任务</button><button data-cancel-ai-song-job ${jobRunning ? "" : "disabled"}>取消当前任务</button></div>
        <pre>${escapeHtml(logs.length ? logs.join("\n") : "暂无任务日志")}</pre>
      </section></main>`;
  }

  if (stageOsCurrentRoute === "song-files") {
    const files = Array.isArray(worker.files) ? worker.files : [];
    return `<main class="os-ai-song-page os-ai-song-files-page">
      <div class="os-workspace-title"><div><div class="os-kicker">创作文件</div><h1>歌曲与干声</h1><p>直接在主程序中查看后台已经生成的本机文件。</p></div><button data-refresh-ai-song>刷新</button></div>
      <section class="os-song-files-card">
        <div class="os-song-files-head"><b>最近 ${files.length} 个文件</b><span>${escapeHtml(worker.default_output_directory || stageOsAiSongForm.output_directory)}</span></div>
        ${files.length ? files.map((file) => `<article><i class="os-fluent">&#xE8D6;</i><div><b>${escapeHtml(file.name)}</b><small>${escapeHtml(file.path)}</small></div><em>${Number(file.size_mb || 0).toFixed(1)} MB</em><time>${file.modified_at ? new Date(file.modified_at).toLocaleString("zh-CN", { hour12: false }) : ""}</time></article>`).join("") : `<div class="os-postlive-blank os-postlive-blank-large"><b>还没有生成文件</b><p>完成歌曲任务或干声录音后会显示在这里。</p></div>`}
      </section></main>`;
  }

  const devices = Array.isArray(recording.devices) ? recording.devices : [];
  return `<main class="os-ai-song-page">
    <div class="os-ai-song-compact-head">
      <div><div class="os-kicker">LOCAL AI SONG STUDIO</div><h1>AI 做歌</h1><p>页面在主程序内；歌曲分离、本人音色转换、自动混音和干声录制在无窗口后台进程运行。</p></div>
      <div class="os-ai-song-compact-status ${statusTone}"><span>${statusLabel}</span><b>${memoryLabel}</b><small>安全启动线 ${minimumMemory.toFixed(0)} GB · 空闲约 95 MB · D 盘能力包不会复制进主软件</small><button data-open-ai-song ${canOpen && !running ? "" : "disabled"}>${primaryLabel}</button><button data-refresh-ai-song>刷新</button></div>
    </div>
    ${liveBlocked ? `<div class="os-ai-song-warning"><i class="os-fluent">&#xE7BA;</i><span><b>正在直播，已启用资源保护</b><small>结束直播后才能启动做歌，避免抢占麦克风、内存和显存。</small></span></div>` : ""}
    ${!installed ? `<div class="os-ai-song-warning error"><i class="os-fluent">&#xEA39;</i><span><b>未找到无头后台组件</b><small>请先完成 AI 做歌后台构建。</small></span></div>` : ""}
    <div class="os-ai-song-workspace">
      <section class="os-song-form-card">
        <div class="os-audio-panel-title"><span>歌曲任务</span><b>${engine.full_ready ? "本地引擎已就绪" : running ? "本地引擎待检查" : "启动后台后检测"}</b></div>
        <div class="os-song-form-grid">
          <label class="span-2"><span>原歌曲</span><div><input data-ai-song-field="song_path" value="${escapeHtml(stageOsAiSongForm.song_path)}" readonly /><button data-choose-ai-song="song">选择</button></div></label>
          <label class="span-2"><span>本人授权音色模型</span><div><input data-ai-song-field="voice_model_path" value="${escapeHtml(stageOsAiSongForm.voice_model_path)}" readonly /><button data-choose-ai-song="model">选择</button></div></label>
          <label class="span-2"><span>音色索引（可选）</span><div><input data-ai-song-field="voice_index_path" value="${escapeHtml(stageOsAiSongForm.voice_index_path)}" readonly /><button data-choose-ai-song="index">选择</button></div></label>
          <label class="span-2"><span>导出目录</span><div><input data-ai-song-field="output_directory" value="${escapeHtml(stageOsAiSongForm.output_directory)}" readonly /><button data-choose-ai-song="output">选择</button></div></label>
          <label><span>质量</span><select data-ai-song-field="quality_preset"><option ${stageOsAiSongForm.quality_preset === "节省显存" ? "selected" : ""}>节省显存</option><option ${stageOsAiSongForm.quality_preset === "均衡" ? "selected" : ""}>均衡</option><option ${stageOsAiSongForm.quality_preset === "高质量" ? "selected" : ""}>高质量</option></select></label>
          <label><span>变调（-24 到 24）</span><input data-ai-song-field="transpose" type="number" min="-24" max="24" value="${Number(stageOsAiSongForm.transpose)}" /></label>
          <label><span>转换人声音量 <b>${Number(stageOsAiSongForm.vocal_volume).toFixed(2)}</b></span><input data-ai-song-field="vocal_volume" type="range" min="0" max="2" step="0.05" value="${Number(stageOsAiSongForm.vocal_volume)}" /></label>
          <label><span>伴奏音量 <b>${Number(stageOsAiSongForm.instrumental_volume).toFixed(2)}</b></span><input data-ai-song-field="instrumental_volume" type="range" min="0" max="2" step="0.05" value="${Number(stageOsAiSongForm.instrumental_volume)}" /></label>
        </div>
        <div class="os-song-submit"><span>${escapeHtml(stageOsAiSongMessage || worker.message || "任务仅使用本机文件")}</span><button data-start-ai-song-job ${liveBlocked || jobRunning || !installed ? "disabled" : ""}>开始生成</button><button data-cancel-ai-song-job ${jobRunning ? "" : "disabled"}>取消</button></div>
      </section>
      <section class="os-song-record-card">
        <div class="os-audio-panel-title"><span>干声录音</span><b>${recording.is_recording ? "正在录音" : recording.state === "saved" ? "已保存" : "待命"}</b></div>
        <label><span>麦克风</span><select data-ai-song-field="recording_device">${devices.length ? devices.map((device) => `<option value="${Number(device.device_number)}">${escapeHtml(device.name)}</option>`).join("") : `<option value="0">启动后台后读取设备</option>`}</select></label>
        <label><span>录音名称</span><input data-ai-song-field="recording_name" value="${escapeHtml(stageOsAiSongForm.recording_name)}" /></label>
        <div class="os-recording-level"><i style="width:${Math.max(0, Math.min(100, Number(recording.level || 0) * 100))}%"></i></div>
        <small>${escapeHtml(recording.last_path || aiSongStatus?.recordings_directory || "D:\\AISongStudioMVP\\data\\recordings")}</small>
        <div><button data-start-ai-recording ${liveBlocked || recording.is_recording || !installed ? "disabled" : ""}>开始录音</button><button data-stop-ai-recording ${recording.is_recording ? "" : "disabled"}>停止并保存</button></div>
      </section>
    </div>
    <section class="os-ai-song-safety"><div><i class="os-fluent">&#xE72E;</i><span><b>与直播主链路隔离</b><small>做歌异常不会成为采集、提醒或安全停止的前置依赖。</small></span></div><div><i class="os-fluent">&#xE950;</i><span><b>后台单实例</b><small>重复操作复用同一管道，不会重复加载模型。</small></span></div><div><i class="os-fluent">&#xE8C7;</i><span><b>所有结果留在本机</b><small>任务与录音文件统一在“创作文件”页查看。</small></span></div></section>
  </main>`;
}

function stageOsAudioPage() {
  const voltune = smartRackStatus?.voltune || {};
  const soundstage = smartRackStatus?.soundstage || {};
  const voltuneOnline = !!voltune.connected;
  const soundOnline = !!soundstage.connected;
  const configuredMode = smartRackStatus?.mode || "none";
  const mode = stageOsAudioModeDraft || configuredMode;
  const modeUsesVoltune = mode === "voltune" || mode === "both";
  const modeUsesSoundStage = mode === "soundstage" || mode === "both";
  const devices = smartRackStatus?.audio_devices || {};
  const microphone = devices.microphone || {};
  const virtualCable = devices.virtual_cable || {};
  const output = devices.output || {};
  const voiceprint = smartRackStatus?.voiceprint || {};
  const audioSettings = smartRackStatus?.audio_settings || {};
  const voiceprintConfigured = !!voiceprint.passed || !!wechatCollectorStatus.voiceprint_configured;
  const voiceprintProgress = Math.max(0, Math.min(3, Number(voiceprint.progress || 0)));
  const activePreset = voltune.preset || "原声";
  const voltuneInputs = Array.isArray(voltune.input_devices) ? voltune.input_devices : [];
  const voltuneOutputs = Array.isArray(voltune.output_devices) ? voltune.output_devices : [];
  const voltunePresets = Array.isArray(voltune.presets) && voltune.presets.length
    ? voltune.presets
    : ["原声", "霓虹电音", "未来机甲", "微醺氛围", "纯伴奏"].map((name) => ({ name }));
  const soundPads = Array.isArray(soundstage.pads) ? soundstage.pads : [];
  const soundCategories = Array.isArray(soundstage.categories) ? soundstage.categories : [];
  const soundOutputs = Array.isArray(soundstage.sound_output_devices) ? soundstage.sound_output_devices : [];
  const selectedPad = soundPads.find((pad) => pad.id === stageOsSelectedPadId) || null;
  const bgm = soundstage.bgm || {};
  const selectedAssistantsReady =
    (!modeUsesVoltune || voltuneOnline) &&
    (!modeUsesSoundStage || soundOnline);
  const audioHeadline = mode === "none"
    ? "音频助手未启用（可选）"
    : selectedAssistantsReady
      ? "已按主播选择配置"
      : "已选择，等待配置";
  return `
    <div class="os-page os-audio-page">
      <div class="os-audio-page-top">
        <div class="os-audio-live-status">
          <span><i></i>${stageOsSessionPhase === "live" ? `正在直播 ${stageOsElapsed()}` : "当前未开播"}</span>
          <b class="${mode === "none" ? "optional" : selectedAssistantsReady ? "ready" : ""}"><i class="os-fluent">&#xE9D9;</i>${audioHeadline}</b>
        </div>
        <button data-stage-nav="live"><i class="os-fluent">&#xE72B;</i>返回直播现场</button>
      </div>
      <div class="os-audio-dashboard">
        <section class="os-audio-signal-panel">
          <div class="os-signal-title">
            <span><i class="os-fluent">&#xE945;</i>麦克风 <b class="os-fluent">&#xE72A;</b> ${modeUsesVoltune ? "VOLTUNE" : "原始声音"} <b class="os-fluent">&#xE72A;</b> 直播输出</span>
            <em>${mode === "none" ? "原始声音直通" : selectedAssistantsReady ? "助手链路已就绪" : "等待完成配置"}</em>
          </div>
          <div class="os-signal-flow">
            ${stageOsSignalNode("&#xE720;", "实时输入", microphone.detail || "麦克风", microphone.detected ? "已识别" : "未检测到")}
            <i class="os-fluent">&#xE72A;</i>
            ${stageOsSignalNode("&#xE9D9;", modeUsesVoltune ? "VOLTUNE" : "音频助手", modeUsesVoltune ? "实时处理" : "主播未启用", modeUsesVoltune ? (voltuneOnline ? "已连接" : "待配置") : "可选")}
            <i class="os-fluent">&#xE72A;</i>
            ${stageOsSignalNode("&#xE7F5;", "直播输出", output.detail || "系统默认输出", output.detected ? "已识别" : "未检测到")}
          </div>
          <div class="os-signal-wave">
            <div class="os-wave-scale"><span>0dB</span><span>-12dB</span><span>-24dB</span><span>-48dB</span></div>
            <canvas data-wave-src="assets/stage-os/waveforms/applause.ogg" aria-label="VOLTUNE预设试听波形"></canvas>
            <div class="os-level-meters"><i></i><i></i><small>L　R</small></div>
          </div>
        </section>
        <section class="os-current-scene">
          <div class="os-current-scene-copy">
            <small><i class="os-fluent">&#xE945;</i> 当前声音场景</small>
            <h2>温柔原声</h2>
            <button data-audio-preset="原声">原声 <i class="os-fluent">&#xE70D;</i></button>
            <b>声音级别 <span>82%</span></b>
            <div class="os-segment-level"><i></i><i></i><i></i><i></i><i></i><i></i></div>
          </div>
          <img src="${stageOsCoachAsset}" alt="AI 教练" />
          <div class="os-integrated-audio-badge"><span><b>LiveCoach 内置控制</b><small>${modeUsesVoltune ? "参数会直接应用到后台音频引擎" : "选择电音模式后即可使用"}</small></span><i class="os-fluent">&#xE73E;</i></div>
        </section>
        <section class="os-audio-voltune-panel">
          <div class="os-audio-panel-title"><span><i class="os-fluent">&#xE945;</i>内置电音引擎 <em>${modeUsesVoltune ? (voltuneOnline ? "已连接" : "待配置") : "未启用"}</em></span><b>完整控制已融入本页</b></div>
          <div class="os-audio-preset-matrix">
            ${voltunePresets.map((preset, index) => stageOsAudioPreset(
              preset.name,
              ["&#xE995;", "&#xE767;", "&#xE950;", "&#xE720;", "&#xE8D7;"][index % 5],
              activePreset === preset.name,
              !voltuneOnline)).join("")}
          </div>
          ${stageOsRange("处理强度", "electro", stageOsAudioValues.electro, !voltuneOnline)}
          ${stageOsRange("音高", "pitch", stageOsAudioValues.pitch, !voltuneOnline)}
          ${stageOsRange("人声削弱", "vocal", stageOsAudioValues.vocal, !voltuneOnline)}
          <div class="os-audio-device-routing">
            <label><span>麦克风输入</span><select data-voltune-input ${voltuneOnline ? "" : "disabled"}>${stageOsDeviceOptions(voltuneInputs, voltune.selected_input_device_id)}</select></label>
            <i class="os-fluent">&#xE72A;</i>
            <label><span>处理后输出</span><select data-voltune-output ${voltuneOnline ? "" : "disabled"}>${stageOsDeviceOptions(voltuneOutputs, voltune.selected_output_device_id)}</select></label>
          </div>
          <div class="os-audio-engine-state"><span>来源：${escapeHtml(voltune.source_mode || "—")}</span><span>捕获：${escapeHtml(voltune.capture_mode || "—")}</span><span>状态：${escapeHtml(voltune.status || "待命")}</span></div>
          <div class="os-audio-panel-actions"><button data-voltune-save-route ${voltuneOnline ? "" : "disabled"}>保存设备</button><button class="primary" data-voltune-start ${voltuneOnline ? "" : "disabled"}>启用麦克风处理</button><button class="danger" data-voltune-stop ${voltuneOnline ? "" : "disabled"}>停止处理</button><button data-configure-audio><i class="os-fluent">&#xE72C;</i>后台重新连接</button></div>
        </section>
        <section class="os-audio-soundstage-panel">
          <div class="os-audio-panel-title"><span><i class="os-fluent">&#xE945;</i>内置音效引擎 <em>${modeUsesSoundStage ? (soundOnline ? "已连接" : "待配置") : "未启用"}</em></span><b>${soundPads.length} 个音效 · 完整设置已融入</b></div>
          <div class="os-audio-search"><i class="os-fluent">&#xE721;</i><input data-audio-search value="${escapeHtml(stageOsAudioSearch)}" placeholder="搜索名称、分类或快捷键" /><label><input data-audio-favorites type="checkbox" ${stageOsAudioFavoriteOnly ? "checked" : ""} />仅看收藏</label><button data-import-audio ${soundOnline ? "" : "disabled"}>＋ 导入音效</button><button data-stop-all-sounds ${soundOnline ? "" : "disabled"}>停止全部</button></div>
          <div class="os-audio-pad-matrix os-audio-full-pads">
            ${soundPads.length
              ? soundPads.map((pad, index) => stageOsFullPad(pad, index, !soundOnline)).join("")
              : `<div class="os-audio-empty">${soundOnline ? "音效库为空，可点击“导入音效”添加" : "连接 SoundStage 后读取完整音效库"}</div>`}
          </div>
          ${stageOsRange("主音量", "masterVolume", stageOsAudioValues.masterVolume, !soundOnline)}
          <div class="os-audio-mixer-settings"><label><span>直播输出设备</span><select data-sound-output ${soundOnline ? "" : "disabled"}>${stageOsSoundOutputOptions(soundOutputs, soundstage.sound_output_device_id)}</select></label><label class="os-switch"><input data-audio-ducking type="checkbox" ${soundstage.ducking ? "checked" : ""} ${soundOnline ? "" : "disabled"} /><i></i><span><b>智能压音</b><small>播放音效时自动降低背景音乐</small></span></label></div>
          <div class="os-bgm-control"><div><i class="os-fluent">&#xE8D6;</i><span><b>${bgm.loaded ? escapeHtml(bgm.name || "背景音乐") : "尚未选择背景音乐"}</b><small>${bgm.loaded ? (bgm.playing ? "正在播放" : "已暂停") : "可直接从本页选择文件"}</small></span></div><button data-import-bgm ${soundOnline ? "" : "disabled"}>选择 BGM</button><button data-toggle-bgm data-playing="${!!bgm.playing}" ${soundOnline && bgm.loaded ? "" : "disabled"}>${bgm.playing ? "暂停" : "播放"}</button>${stageOsRange("BGM 音量", "bgmVolume", stageOsAudioValues.bgmVolume, !soundOnline || !bgm.loaded)}</div>
          ${selectedPad ? stageOsPadEditor(selectedPad, soundCategories) : ""}
        </section>
        <section class="os-audio-device-panel">
          <div class="os-audio-panel-title"><span><i class="os-fluent">&#xE713;</i>设备与助手</span><em>按需使用，不影响 AI 教练</em></div>
          <div class="os-assistant-mode" role="radiogroup" aria-label="音频助手使用方式">
            ${stageOsAssistantModeOption("none", "不使用", "不启用内置电音与音效", mode)}
            ${stageOsAssistantModeOption("voltune", "只用电音", "LiveCoach 内置电音", mode)}
            ${stageOsAssistantModeOption("soundstage", "只用音效", "LiveCoach 内置音效", mode)}
            ${stageOsAssistantModeOption("both", "两者都用", "完整内置音频工具", mode)}
          </div>
          <div class="os-audio-setup-actions">
            <button class="primary" data-configure-audio>${mode === "none" ? "保存选择" : "启用并后台连接"}</button>
            <button data-refresh-prep>重新检测</button>
          </div>
          <div class="os-device-list">
            ${stageOsDeviceRow("&#xE720;", "麦克风", microphone)}
            ${stageOsDeviceRow("&#xE95E;", "虚拟声卡", virtualCable)}
            ${stageOsDeviceRow("&#xE7F5;", "输出设备", output)}
          </div>
          <div class="os-voiceprint-status ${voiceprintConfigured ? "configured" : ""} ${voiceprint.running ? "running" : ""}">
            <span>主播声纹 <b>${escapeHtml(voiceprint.status_label || (voiceprintConfigured ? "已校准" : "未校准"))}</b></span>
            <h3>${escapeHtml(voiceprint.prompt_title || "请先选择并保存主播实际使用的实体麦克风")}</h3>
            <p>${escapeHtml(voiceprint.prompt_text || "校准开始后，按界面顺序自然朗读三句话，每句话说完停顿一下。")}</p>
            <div class="os-voiceprint-progress" aria-label="声纹校准进度"><i class="${voiceprintProgress >= 1 ? "done" : ""}"></i><i class="${voiceprintProgress >= 2 ? "done" : ""}"></i><i class="${voiceprintProgress >= 3 ? "done" : ""}"></i></div>
            <small>${escapeHtml(voiceprint.metrics || "有效语音 0/3 · 转写 0/3 · 音量待检测")}</small>
            <div class="os-voiceprint-actions">
              <button data-start-voiceprint ${!voltuneOnline || voiceprint.running ? "disabled" : ""}>${voiceprintConfigured ? "重新校准" : "开始声纹校准"}</button>
              ${voiceprint.running ? `<button data-cancel-voiceprint>取消</button>` : ""}
            </div>
          </div>
          <div class="os-audio-background-settings" data-audio-settings>
            <span>后台进程设置</span>
            <label><small>电音引擎相对路径</small><input data-audio-voltune-path value="${escapeHtml(audioSettings.voltune_executable || "tools/voltune/Voltune.ElectroAssistant.exe")}" /></label>
            <label><small>音效引擎相对路径</small><input data-audio-soundstage-path value="${escapeHtml(audioSettings.soundstage_executable || "tools/soundstage/声场.exe")}" /></label>
            <label><small>状态刷新（2–15 秒）</small><input data-audio-refresh-seconds type="number" min="2" max="15" value="${Number(audioSettings.status_refresh_seconds || 3)}" /></label>
            <label class="check"><input data-audio-keep-voltune type="checkbox" ${audioSettings.keep_voltune_on_exit !== false ? "checked" : ""} /><small>退出后保持电音引擎</small></label>
            <label class="check"><input data-audio-keep-soundstage type="checkbox" ${audioSettings.keep_soundstage_on_exit !== false ? "checked" : ""} /><small>退出后保持音效引擎</small></label>
            <div><button data-save-audio-settings>保存后台设置</button><small>${escapeHtml(stageOsAudioSettingsMessage)}</small></div>
          </div>
        </section>
      </div>
      ${stageOsVirtualMixerPanel()}
      <div class="os-audio-page-route">
        <span><i class="os-fluent">&#xE720;</i><b>麦克风</b><small>本机输入</small></span>
        <i class="os-fluent">&#xE72A;</i>
        <span><i class="os-fluent">&#xE9D9;</i><b>${stageOsAudioModeLabel(mode)}</b><small>${mode === "none" ? "不接管现有音频" : "主播按需启用"}</small></span>
        <i class="os-fluent">&#xE72A;</i>
        <span><i class="os-fluent">&#xE7F5;</i><b>直播输出</b><small>系统默认输出</small></span>
        <strong class="${mode === "none" ? "optional" : selectedAssistantsReady ? "ready" : ""}"><i class="os-fluent">&#xE73E;</i>${audioHeadline}</strong>
        <button data-configure-audio>${mode === "none" ? "启用内置音频" : "后台重新连接"}</button>
      </div>
    </div>
  `;
}

function stageOsVirtualMixerPanel() {
  const vm = stageOsVirtualMixer || {};
  const s = vm.settings || {};
  const status = vm.status || {};
  const devices = Array.isArray(vm.devices) ? vm.devices : [];
  const inputs = devices.filter((item) => Number(item.direction) === 0);
  const outputs = devices.filter((item) => Number(item.direction) === 1);
  const number = (key, label, min, max, step = 1) => `<label><span>${label}</span><input data-vm-field="${key}" type="number" min="${min}" max="${max}" step="${step}" value="${Number(s[key] ?? 0)}" /></label>`;
  const check = (key, label) => `<label class="check"><input data-vm-field="${key}" type="checkbox" ${s[key] ? "checked" : ""} /><span>${label}</span></label>`;
  const deviceOptions = (items, selected, empty) => `<option value="">${empty}</option>${items.map((item) => `<option value="${escapeHtml(item.id)}" ${item.id === selected ? "selected" : ""}>${escapeHtml(item.name)}${item.is_likely_virtual_endpoint ? " · 虚拟端点" : ""}</option>`).join("")}`;
  const presetLabels = { Original: "原声", ChatClear: "聊天清晰", LiveWarm: "直播温暖", SingingNatural: "自然演唱", HighEnergy: "高能互动", NoiseReductionOnly: "仅降噪" };
  const trial = status.trial_result || null;
  return `
    <section class="os-virtual-mixer" data-virtual-mixer>
      <header><div><span>虚拟声卡与本地 DSP</span><h2>高级调音通路</h2><p>原 AiVirtualMixer 完整能力已迁入本页；音频处理仍在独立无窗口 Engine 中运行。</p></div><b class="${vm.online ? "online" : ""}">${vm.online ? (status.is_running ? "通路运行中" : "后台已连接") : "按需启动"}</b></header>
      <div class="os-vm-status"><span>输入 ${Number(status.input_peak_db ?? -96).toFixed(1)} dB</span><span>输出 ${Number(status.output_peak_db ?? -96).toFixed(1)} dB</span><span>音高 ${Number(status.detected_pitch_hz || 0).toFixed(1)} Hz</span><span>削波 ${Number(status.clip_count || 0)}</span><span>预计延迟 ${Number(status.estimated_latency_ms || 0)} ms</span><em>${escapeHtml(vm.message || status.message || "待命")}</em></div>
      <div class="os-vm-presets">${(vm.presets || Object.keys(presetLabels)).map((name) => `<button data-vm-preset="${name}">${presetLabels[name] || name}</button>`).join("")}</div>
      <div class="os-vm-grid">
        <fieldset><legend>设备与电平</legend>
          <label><span>真实麦克风</span><select data-vm-field="input_device_id">${deviceOptions(inputs, s.input_device_id, "选择输入设备")}</select></label>
          <label><span>虚拟麦克风输出</span><select data-vm-field="virtual_output_device_id">${deviceOptions(outputs, s.virtual_output_device_id, "选择虚拟输出端点")}</select></label>
          <label><span>监听输出（可选）</span><select data-vm-field="monitor_output_device_id">${deviceOptions(outputs, s.monitor_output_device_id, "不监听")}</select></label>
          ${number("buffer_milliseconds", "缓冲 ms", 10, 100, 10)}${number("input_gain_db", "输入增益 dB", -24, 24, .5)}${number("output_gain_db", "输出增益 dB", -24, 24, .5)}
          ${check("muted", "静音")}${check("level_match_enabled", "等响度补偿")}${check("bypass", "旁路")}
        </fieldset>
        <fieldset><legend>清理与均衡</legend>
          ${check("high_pass_enabled", "高通滤波")}${number("high_pass_hz", "高通 Hz", 40, 240, 5)}
          ${check("noise_gate_enabled", "柔和噪声门")}${number("gate_threshold_db", "噪声门阈值 dB", -80, -20, 1)}
          ${check("noise_reduction_enabled", "频谱降噪")}${number("noise_reduction_strength", "降噪强度", 0, 1, .05)}${number("noise_reduction_naturalness", "自然度", 0, 1, .05)}${number("noise_reduction_max_db", "最大降噪 dB", 0, 30, 1)}
          ${number("low_eq_db", "低频 EQ dB", -12, 12, .5)}${number("presence_eq_db", "存在感 EQ dB", -12, 12, .5)}${number("air_eq_db", "空气感 EQ dB", -12, 12, .5)}
        </fieldset>
        <fieldset><legend>动态与空间</legend>
          ${check("compressor_enabled", "压缩器")}${number("compressor_threshold_db", "压缩阈值 dB", -60, 0, 1)}${number("compressor_ratio", "压缩比", 1, 12, .5)}
          ${check("de_esser_enabled", "去齿音")}${number("de_esser_strength", "去齿音强度", 0, 1, .05)}${check("limiter_enabled", "限幅保护")}
          ${check("reverb_enabled", "房间混响")}${number("reverb_mix", "混响比例", 0, 1, .01)}${number("reverb_room_size", "房间大小", 0, 1, .05)}${number("reverb_decay", "衰减", 0, 1, .05)}${number("reverb_pre_delay_ms", "预延迟 ms", 0, 100, 1)}${number("reverb_low_cut_hz", "混响低切 Hz", 20, 1000, 10)}${number("reverb_high_cut_hz", "混响高切 Hz", 1000, 20000, 100)}
        </fieldset>
        <fieldset><legend>实验性修音与试音</legend>
          ${check("pitch_correction_enabled", "实时修音")}${number("pitch_correction_strength", "修音强度", 0, 1, .05)}${number("pitch_retune_speed", "修正速度", 0, 1, .05)}${number("pitch_key", "调性（0=C）", 0, 11, 1)}
          <label><span>音阶</span><select data-vm-field="pitch_scale"><option value="0" ${Number(s.pitch_scale) === 0 ? "selected" : ""}>半音阶</option><option value="1" ${Number(s.pitch_scale) === 1 ? "selected" : ""}>大调</option><option value="2" ${Number(s.pitch_scale) === 2 ? "selected" : ""}>小调</option></select></label>
          <div class="os-vm-trial"><b>${status.trial_seconds_remaining > 0 ? `试音剩余 ${status.trial_seconds_remaining} 秒` : trial?.summary || "15 秒本地 AI 试音"}</b><p>${trial ? `响度 ${Number(trial.average_loudness_db).toFixed(1)} dB · 峰值 ${Number(trial.peak_db).toFixed(1)} dB · 信噪比 ${Number(trial.signal_to_noise_db).toFixed(1)} dB` : "采样只保存在内存中，不保存原始音频。"}</p>${trial?.reasons?.length ? `<ul>${trial.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join("")}</ul>` : ""}</div>
        </fieldset>
      </div>
      <footer><button data-vm-action="devices">刷新设备</button><button class="primary" data-vm-action="start">启动通路</button><button data-vm-action="update">应用参数</button><button data-vm-action="bypass">${status.is_bypassed ? "关闭旁路" : "开启旁路"}</button><button data-vm-action="trial" ${status.is_running ? "" : "disabled"}>开始 15 秒试音</button><button data-vm-action="recommend" ${trial ? "" : "disabled"}>应用试音建议</button><button data-vm-action="save">保存个人预设</button><button class="danger" data-vm-action="stop">停止通路</button></footer>
    </section>`;
}

function stageOsSignalNode(icon, title, detail, status) {
  return `
    <div class="os-signal-node">
      <small>${title}</small><i class="os-fluent">${icon}</i><b>${detail}</b><span>${status}</span>
    </div>
  `;
}

function stageOsAudioPreset(name, icon, selected, disabled = false) {
  return `<button class="${selected ? "selected" : ""}" data-audio-preset="${name}" ${disabled ? "disabled" : ""}><i class="os-fluent">${icon}</i><b>${name}</b></button>`;
}

function stageOsCompactPad(name, number, icon, file, disabled = false) {
  return `
    <button data-sound-pad="${name}" ${disabled ? "disabled" : ""}>
      <small>${number}</small><i class="os-fluent">${icon}</i><b>${name}</b>
      <canvas data-wave-src="assets/stage-os/waveforms/${file}" aria-label="${name}音效波形"></canvas>
    </button>
  `;
}

function stageOsDeviceOptions(devices, selectedId) {
  if (!devices.length) return `<option value="">暂无可用设备</option>`;
  return devices.map((device) => `
    <option value="${Number(device.id)}" ${Number(device.id) === Number(selectedId) ? "selected" : ""}>
      ${escapeHtml(device.name || "未命名设备")}${device.channels ? ` · ${device.channels}声道` : ""}${device.is_default ? " · 默认" : ""}
    </option>`).join("");
}

function stageOsSoundOutputOptions(devices, selectedId) {
  if (!devices.length) return `<option value="default">系统默认输出</option>`;
  return devices.map((device) => `
    <option value="${escapeHtml(device.id || "default")}" ${(device.id || "default") === (selectedId || "default") ? "selected" : ""}>
      ${escapeHtml(device.name || "系统默认输出")}${device.is_default ? " · 默认" : ""}
    </option>`).join("");
}

function stageOsFullPad(pad, index, disabled = false) {
  const waveforms = ["celebration.ogg", "applause.ogg", "correct.ogg", "gift-chime.ogg", "reveal.ogg", "drum-roll.ogg"];
  const playing = !!pad.playing;
  const number = String(index + 1).padStart(2, "0");
  return `
    <article class="os-full-pad ${playing ? "playing" : ""}" data-sound-pad-card data-favorite="${!!pad.favorite}"
             data-search-text="${escapeHtml(`${pad.name || ""} ${pad.category || ""} ${pad.shortcut || ""}`)}" ${disabled ? "aria-disabled=true" : ""}>
      <button class="os-pad-play" data-pad-id="${escapeHtml(pad.id || "")}" ${disabled ? "disabled" : ""}>
        <small>${number}</small><i class="os-fluent">${playing ? "&#xE769;" : "&#xE768;"}</i><b>${escapeHtml(pad.name || "音效")}</b>
        <canvas data-wave-src="assets/stage-os/waveforms/${waveforms[index % waveforms.length]}" aria-label="${escapeHtml(pad.name || "音效")}波形"></canvas>
        <span>${escapeHtml(pad.category || "未分类")} · ${Math.round(Number(pad.volume || 0) * 100)}%</span>
      </button>
      <div><em>${pad.favorite ? "★ 收藏" : stageOsPadModeLabel(pad.mode)}</em>${playing ? `<button data-stop-pad="${escapeHtml(pad.id || "")}">停止</button>` : ""}<button data-edit-pad="${escapeHtml(pad.id || "")}">设置</button></div>
    </article>`;
}

function stageOsPadModeLabel(mode) {
  if (mode === "overlap") return "允许叠加";
  if (mode === "toggle") return "播放/停止";
  if (mode === "loop") return "循环播放";
  return "重新播放";
}

function stageOsPadEditor(pad, categories) {
  const categoryList = [...new Set([...(categories || []), pad.category || "自定义", "自定义"])];
  const colors = ["orange", "yellow", "green", "pink", "blue", "purple", "cyan", "slate"];
  return `
    <section class="os-pad-editor" data-pad-editor="${escapeHtml(pad.id || "")}">
      <div class="os-pad-editor-head"><span><i class="os-fluent">&#xE713;</i><b>音效详细设置</b><small>${escapeHtml(pad.name || "音效")}</small></span><button data-edit-pad="">关闭</button></div>
      <div class="os-pad-editor-grid">
        <label><span>音效名称</span><input data-pad-name maxlength="36" value="${escapeHtml(pad.name || "")}" /></label>
        <label><span>分类</span><select data-pad-category>${categoryList.map((category) => `<option ${category === pad.category ? "selected" : ""}>${escapeHtml(category)}</option>`).join("")}</select></label>
        <label><span>触发方式</span><select data-pad-mode><option value="restart" ${pad.mode === "restart" ? "selected" : ""}>重新播放</option><option value="overlap" ${pad.mode === "overlap" ? "selected" : ""}>允许叠加</option><option value="toggle" ${pad.mode === "toggle" ? "selected" : ""}>播放/停止</option><option value="loop" ${pad.mode === "loop" ? "selected" : ""}>循环播放</option></select></label>
        <label><span>全局快捷键</span><input data-pad-shortcut value="${escapeHtml(pad.shortcut || "")}" placeholder="例如 Ctrl+Alt+1" /></label>
        <label><span>按钮颜色</span><select data-pad-color>${colors.map((color) => `<option value="${color}" ${color === pad.color ? "selected" : ""}>${color}</option>`).join("")}</select></label>
        <label><span>独立音量 · ${Math.round(Number(pad.volume || 0) * 100)}%</span><input data-pad-volume type="range" min="0" max="100" value="${Math.round(Number(pad.volume || 0) * 100)}" /></label>
      </div>
      <div class="os-pad-editor-actions"><label><input data-pad-favorite type="checkbox" ${pad.favorite ? "checked" : ""} />加入常用收藏</label><button class="danger" data-delete-pad="${escapeHtml(pad.id || "")}">删除音效</button><button class="primary" data-save-pad-settings>保存详细设置</button></div>
    </section>`;
}

function stageOsAssistantModeOption(value, label, detail, current) {
  const selected = value === current;
  return `
    <button class="${selected ? "selected" : ""}" data-audio-mode="${value}" role="radio" aria-checked="${selected}">
      <i class="os-fluent">${selected ? "&#xE73E;" : "&#xE739;"}</i>
      <span><b>${label}</b><small>${detail}</small></span>
    </button>
  `;
}

function stageOsAudioModeLabel(mode) {
  if (mode === "voltune") return "VOLTUNE";
  if (mode === "soundstage") return "SoundStage";
  if (mode === "both") return "两款音频助手";
  return "原始音频";
}

function stageOsDeviceRow(icon, title, device) {
  const detected = !!device?.detected;
  const label = device?.label || (detected ? "已识别" : "未检测到");
  const detail = device?.detail || "Windows 设备";
  return `
    <div class="os-device-row">
      <i class="os-fluent">${icon}</i><span><b>${title}</b><small>${escapeHtml(detail)}</small></span><em class="${detected ? "online" : "warning"}">${escapeHtml(label)}</em>
    </div>
  `;
}

function stageOsOnlineBadge(label, connected) {
  return `<span class="${connected ? "online" : ""}"><i></i>${label} ${connected ? "在线" : "离线"}</span>`;
}

function stageOsRange(label, action, value, disabled = false) {
  return `
    <label class="os-range-control ${disabled ? "disabled" : ""}" style="--value:${value}%">
      <span>${label}<b>${value}</b></span>
      <input type="range" min="0" max="100" value="${value}" data-audio-range="${action}" ${disabled ? "disabled" : ""} />
    </label>
  `;
}

function stageOsSoundButton(name, category, number) {
  return `<button class="os-effect-button" data-sound-pad="${name}"><span>${number}</span><b>${name}</b><small>${category}</small></button>`;
}

function stageOsProfilePage() {
  activeStep = 1;
  return `
    <div class="os-page os-form-page">
      <div class="os-form-heading">
        <div><div class="os-kicker">个人档案</div><h1>让建议更像你本人</h1><p>只保留真正会改变话术的三项，随时可以回来调整。</p></div>
        <button class="os-primary" data-save-stage-profile>保存档案</button>
      </div>
      <div class="os-profile-grid">
        <section class="os-form-section">
          <h2>主播身份</h2>
          <div class="form-grid">
            ${field("主播名称", "anchor_name", "例：小美 / 玖儿")}
            ${field("主播 ID", "anchor_id", "可留空，系统自动生成")}
          </div>
        </section>
        <section class="os-profile-summary">
          <img src="${stageOsCoachAsset}" alt="" />
          <div>
            <small>当前建议气质</small>
            <b>${escapeHtml(form.main_persona || "尚未选择")}</b>
            <span>${escapeHtml(form.relationship_feel || "选择和观众的距离感")}</span>
            <em>只影响表达方式，不替主播编造经历。</em>
          </div>
        </section>
        <section class="os-form-section os-profile-options span-2">
          ${optionGroup("表达主调", "main_persona", "single", optionGroups.main_persona, "span-2")}
          ${optionGroup("情绪外放动作", "supporting_traits", "single", optionGroups.supporting_traits, "span-2")}
          ${optionGroup("与观众的关系感", "relationship_feel", "single", optionGroups.relationship_feel, "span-2")}
        </section>
      </div>
    </div>
  `;
}

function stageOsSettingsPage() {
  if (stageOsCurrentRoute === "account") return stageOsAccountPage();
  if (stageOsCurrentRoute === "diagnostics") return stageOsDiagnosticsPage();
  if (["ai-model", "privacy", "about"].includes(stageOsCurrentRoute)) {
    return stageOsSystemInfoPage(stageOsCurrentRoute);
  }
  activeStep = 7;
  const collectorSessionLost = ["session_lost", "stalled"].includes(wechatCollectorStatus.collector_state);
  const collectorHealthy = !!wechatCollectorStatus.collector_attached && !collectorSessionLost;
  const collectorSettingState = collectorSessionLost
    ? "需登录"
    : wechatCollectorStatus.collector_receiving
      ? "接收中"
      : wechatCollectorStatus.collector_attached
        ? "监听中"
        : "未启动";
  return `
    <div class="os-page os-settings-page">
      <div class="os-form-heading">
        <div><div class="os-kicker">系统设置</div><h1>连接、安全与辅助工具</h1><p>低频设置收在这里，不占用直播现场。</p></div>
        <button class="os-primary" data-save-stage-settings>保存设置</button>
      </div>
      <div class="os-settings-grid">
        <section class="os-settings-card">
          <span>本机通道</span><h2>主播端分析连接</h2>
          <div class="form-grid">
            ${field("Relay 地址", "relay_url", "http://127.0.0.1:8787")}
            ${field("Room", "room", "demo")}
            ${field("连接 Key", "relay_key", "无密钥可留空", "span-2")}
          </div>
        </section>
        <section class="os-settings-card os-status-card">
          <div class="os-status-card-head">
            <div><span>工具与数据</span><h2>本机运行状态</h2></div>
            <b>${wechatCollectorStatus.local_only !== false ? "本机模式" : "远程模式"}</b>
          </div>
          <div class="os-setting-row"><i class="os-fluent">&#xE9D9;</i><div><b>直播数据助手</b><small>${escapeHtml(wechatCollectorStatus.collector_state_label || wechatCollectorStatus.collector_status)}</small></div><em class="${collectorHealthy ? "online" : collectorSessionLost ? "warning" : ""}">${collectorSettingState}</em><button data-open-collector-inline>打开</button></div>
          <div class="os-setting-row"><i class="os-fluent">&#xE945;</i><div><b>AI 分析引擎</b><small>${escapeHtml(wechatCollectorStatus.analysis_status)}</small></div><em class="${wechatCollectorStatus.analysis_running ? "online" : ""}">${wechatCollectorStatus.analysis_running ? "运行中" : "检测中"}</em><button data-refresh-prep>检测</button></div>
          <label class="os-setting-row os-setting-toggle"><i class="os-fluent">&#xE8B9;</i><div><b>Qwen 画面理解</b><small>低频读取本机截图；默认关闭，不影响核心分析</small></div><em class="${form.vision_semantic_enabled ? "online" : "optional"}">${form.vision_semantic_enabled ? "已开启" : "已关闭"}</em><input data-bind="vision_semantic_enabled" type="checkbox" ${form.vision_semantic_enabled ? "checked" : ""} /></label>
          <div class="os-setting-row"><i class="os-fluent">&#xE995;</i><div><b>音频工作台</b><small>${escapeHtml(smartRackStatus.connection)}</small></div><em class="${smartRackStatus.tone === "online" ? "online" : smartRackStatus.tone === "optional" ? "optional" : ""}">${smartRackStatus.tone === "online" ? "在线" : smartRackStatus.tone === "optional" ? "可选" : "待配置"}</em><button data-open-audio>查看</button></div>
          <div class="os-setting-row"><i class="os-fluent">&#xE7C4;</i><div><b>主播悬浮窗</b><small>提醒内容只在主播端展示</small></div><em class="${stageOsOverlayVisible ? "online" : ""}">${stageOsOverlayVisible ? "已开启" : "已关闭"}</em><button data-toggle-overlay>${stageOsOverlayVisible ? "关闭" : "打开"}</button></div>
        </section>
        <section class="os-privacy-card span-2">
          <div><b>数据边界</b><span>LOCAL FIRST</span></div>
          <p>直播事件、转写与验收日志保留在主播电脑；主播端不保存云端 API Key，也不会自动发送私信。</p>
          <ul><li>日志留在本机</li><li>私信由主播确认</li><li>旧工具仅主动打开</li></ul>
        </section>
      </div>
    </div>
  `;
}

function stageOsAccountPage() {
  activeStep = 7;
  const account = stageOsAccount || {};
  const expires = Number(account.expires_at_ms || 0) > 0
    ? new Date(Number(account.expires_at_ms)).toLocaleString("zh-CN", { hour12: false })
    : "—";
  const used = Number(account.daily_request_count || 0);
  const limit = Number(account.daily_request_limit || 0);
  const progress = limit > 0 ? Math.max(0, Math.min(100, Math.round(used / limit * 100))) : 0;
  if (!account.configured) {
    return `
      <div class="os-page os-settings-page os-account-page">
        <div class="os-form-heading"><div><div class="os-kicker">账号与会员</div><h1>商业账号服务尚未启用</h1><p>当前开发版继续运行本机低风险规则，不会读取或下发云端豆包主 Key。</p></div></div>
        <section class="os-account-boundary"><b>安全状态</b><p>${escapeHtml(account.message || "等待配置HTTPS商业网关。")}</p><ul><li>安装包不包含服务端 Key</li><li>未登录不会产生我们的模型费用</li><li>尚未接入自动支付</li></ul></section>
      </div>`;
  }
  if (account.logged_in) {
    return `
      <div class="os-page os-settings-page os-account-page">
        <div class="os-form-heading"><div><div class="os-kicker">账号与会员</div><h1>${escapeHtml(account.display_name || "主播账号")}</h1><p>轻账号绑定激活权益、设备和云端AI额度；转发安装包不会转移授权。</p></div><button data-account-refresh ${stageOsAccountBusy ? "disabled" : ""}>刷新状态</button></div>
        <div class="os-account-summary">
          <section><span>当前账号 ID</span><h2>${escapeHtml(account.account_id || "—")}</h2><p>${escapeHtml(account.device_name || "本机设备")}</p></section>
          <section><span>当前权益</span><h2>${escapeHtml(account.plan || "trial")}</h2><p>${account.managed_ai_ready ? "托管 AI 可用" : "授权已到期或暂停"}</p></section>
          <section><span>权益有效期</span><h2>${escapeHtml(expires)}</h2><p>最多绑定 ${Number(account.max_devices || 0)} 台设备</p></section>
        </div>
        <section class="os-account-usage">
          <div><span>今日AI保险丝</span><b>${used} / ${limit || "—"} 次</b></div>
          <i><em style="width:${progress}%"></em></i>
          <p>输入 ${Number(account.daily_input_tokens || 0).toLocaleString("zh-CN")} tokens · 输出 ${Number(account.daily_output_tokens || 0).toLocaleString("zh-CN")} tokens。达到上限后服务端自动停止，不会继续消耗豆包费用。</p>
        </section>
        <section class="os-account-boundary"><b>Key 安全边界</b><p>主播端只持有短期、可撤销、受额度限制的AI会话凭证；豆包主 Key 只存在服务器密钥环境中。</p><ul><li>账号与设备双重校验</li><li>单设备并发与频率限制</li><li>每日额度和全局月预算</li></ul></section>
        ${stageOsAccountMessage ? `<p class="os-account-message">${escapeHtml(stageOsAccountMessage)}</p>` : ""}
        ${stageOsRecoveryCode ? `<section class="os-account-recovery"><span>仅显示一次的账号恢复码</span><code>${escapeHtml(stageOsRecoveryCode)}</code><p>换电脑或重装后需要它。请立即离线保存；服务端只保存哈希。界面最多保留 2 分钟，复制后 60 秒自动隐藏并清理剪贴板。</p><button data-copy-account-recovery>复制恢复码</button></section>` : ""}
        <div class="os-account-footer"><button class="danger" data-account-logout ${stageOsAccountBusy ? "disabled" : ""}>退出并清除本机授权</button></div>
      </div>`;
  }
  return `
    <div class="os-page os-settings-page os-account-page">
      <div class="os-form-heading"><div><div class="os-kicker">账号与会员</div><h1>激活后启用托管 AI</h1><p>不使用邮箱密码。设备密钥和短期会话只由原生端处理，并使用 Windows 当前用户 DPAPI 保存。</p></div></div>
      <div class="os-account-auth-grid">
        <form data-account-activate autocomplete="off">
          <span>首次使用</span><h2>激活试用轻账号</h2>
          <label><b>主播称呼</b><input name="display_name" maxlength="40" required placeholder="用于账号界面" /></label>
          <label><b>一次性激活码</b><input data-account-sensitive name="activation_code" required maxlength="160" autocomplete="off" /></label>
          <button class="os-primary" type="submit" ${stageOsAccountBusy ? "disabled" : ""}>激活并绑定本机</button>
        </form>
        <form data-account-recover autocomplete="off">
          <span>换机或重装</span><h2>使用恢复码找回</h2>
          <label><b>账号恢复码</b><input data-account-sensitive name="recovery_code" required maxlength="160" autocomplete="off" /></label>
          <p>恢复后会绑定这台电脑，并轮换本机设备密钥；超过设备上限时需要人工解绑。</p>
          <button type="submit" ${stageOsAccountBusy ? "disabled" : ""}>恢复并绑定本机</button>
        </form>
      </div>
      ${stageOsAccountMessage ? `<p class="os-account-message">${escapeHtml(stageOsAccountMessage)}</p>` : ""}
      <section class="os-account-boundary"><b>内测阶段说明</b><p>当前只开放一次性激活码和人工权益开通，尚未接入付款；避免公开试用被批量盗刷。</p><ul><li>随机安装 ID，不上传完整硬件序列号</li><li>主播称呼与 Windows 计算机名会写入账号服务，用于显示和设备识别</li><li>一个账号默认最多 2 台设备；不收集邮箱和用户密码</li></ul></section>
    </div>`;
}

function stageOsSystemInfoPage(route) {
  const page = {
    "ai-model": {
      kicker: "AI 与模型",
      title: "既有推理链路状态",
      description: "这里只查看现有能力是否可用；不在本次架构整理中修改模型、提示词或判断门槛。",
      cards: [
        ["直播分析", wechatCollectorStatus.analysis_running ? "运行中" : "按直播需要启动", wechatCollectorStatus.analysis_status || "本机分析服务"],
        ["画面理解", form.vision_semantic_enabled ? "已开启" : "默认关闭", "Qwen 低频本机画面理解，不影响核心分析"],
        ["大盘分析", stageOsBigBoard.model || "沿用当前配置", "仅显示现有模型状态，不改变选择逻辑"]
      ]
    },
    privacy: {
      kicker: "隐私与数据",
      title: "本机优先的数据边界",
      description: "保持现有隐私约束：直播数据和验收记录留在本机，私信草稿必须人工确认。",
      cards: [
        ["直播事件与转写", "保留在本机", "由主播电脑上的服务处理"],
        ["API 凭据", "不写入主播档案", "继续使用现有安全存储与进程边界"],
        ["观众承接", "不会自动发送", "只生成草稿并等待主播人工操作"]
      ]
    },
    about: {
      kicker: "关于与版本",
      title: "LiveCoach AI 主播工作台",
      description: "单窗口、单入口、多页面；分析、采集、音频与创作服务按需在后台独立运行。",
      cards: [
        ["可见窗口", "主程序 + 提示悬浮窗", "其他功能全部通过主菜单和侧边栏进入"],
        ["后台服务", "独立失效、统一监管", "单个服务异常不会拖垮直播主链路"],
        ["当前阶段", "架构整合开发版", "未覆盖正式发布目录或桌面快捷方式"]
      ]
    }
  }[route];
  return `
    <div class="os-page os-settings-page os-system-info-page" data-system-page="${route}">
      <div class="os-form-heading"><div><div class="os-kicker">${page.kicker}</div><h1>${page.title}</h1><p>${page.description}</p></div></div>
      <div class="os-system-info-grid">
        ${page.cards.map(([title, state, detail]) => `<section><span>${title}</span><h2>${escapeHtml(state)}</h2><p>${escapeHtml(detail)}</p></section>`).join("")}
      </div>
    </div>
  `;
}

function stageOsDiagnosticsPage() {
  const payload = stageOsDemoState || {};
  const state = payload.state || {};
  const scenarios = Array.isArray(payload.scenarios) ? payload.scenarios : [];
  const logs = Array.isArray(state.logs) ? state.logs : [];
  const progress = Number(state.step_count || 0) > 0
    ? Math.round(Number(state.step_index || 0) / Number(state.step_count) * 100)
    : 0;
  const worker = aiSongStatus?.worker || {};
  const serviceRow = (name, running, detail, optional = false) => `
    <article><i class="${running ? "online" : optional ? "optional" : ""}"></i><span><b>${name}</b><small>${escapeHtml(detail || "—")}</small></span><em>${running ? "运行中" : optional ? "按需启动" : "未运行"}</em></article>`;
  return `
    <div class="os-page os-settings-page os-diagnostics-page" data-demo-controller>
      <div class="os-form-heading">
        <div><div class="os-kicker">诊断中心</div><h1>演示与本机状态</h1><p>虚构演示数据与正式直播完全隔离；只在诊断页手动运行。</p></div>
        <button data-refresh-prep>刷新运行状态</button>
      </div>
      <div class="os-diagnostics-grid">
        <section class="os-background-services-card">
          <div><span>后台服务监管</span><b>主程序统一启动、检查与安全退出</b></div>
          ${serviceRow("直播分析", !!wechatCollectorStatus.analysis_running, wechatCollectorStatus.analysis_status || "等待开播")}
          ${serviceRow("视频号采集", !!wechatCollectorStatus.collector_attached, wechatCollectorStatus.collector_status || "等待连接")}
          ${serviceRow("VOLTUNE", !!smartRackStatus.voltune?.connected, smartRackStatus.voltune?.detail || "主播按需启用", !smartRackStatus.voltune?.connected)}
          ${serviceRow("SoundStage", !!smartRackStatus.soundstage?.connected, smartRackStatus.soundstage?.detail || "主播按需启用", !smartRackStatus.soundstage?.connected)}
          ${serviceRow("虚拟调音", !!stageOsVirtualMixer.online, stageOsVirtualMixer.status?.message || "主播按需启用", !stageOsVirtualMixer.online)}
          ${serviceRow("AI 做歌", !!aiSongStatus?.running, worker.stage || "主播按需启用", !aiSongStatus?.running)}
        </section>
        <section class="os-demo-controller-card">
          <div class="os-demo-card-head"><div><span>AI 教练演示</span><h2>场景播放器</h2></div><b class="${state.running ? "running" : ""}">${state.running ? (state.paused ? "已暂停" : "运行中") : "已停止"}</b></div>
          <div class="os-demo-fields">
            <label><span>测试场景</span><select data-demo-scenario>${(scenarios.length ? scenarios : [{ id: "new-viewer", title: "1. 新观众进入、停留并扣字" }]).map((item) => `<option value="${escapeHtml(item.id)}" ${item.id === state.scenario ? "selected" : ""}>${escapeHtml(item.title)}</option>`).join("")}</select></label>
            <label><span>运行模式</span><select data-demo-mode><option value="fixed" ${state.mode !== "real" ? "selected" : ""}>固定回放</option><option value="real" ${state.mode === "real" ? "selected" : ""}>真实 AI 推理</option></select></label>
            <label><span>方向延迟</span><input data-demo-delay type="range" min="1" max="3" step="0.2" value="${Number(state.delay_seconds || 1.8)}" /><small>${Number(state.delay_seconds || 1.8).toFixed(1)} 秒</small></label>
          </div>
          <div class="os-demo-actions">
            <button class="primary" data-demo-action="start">开始</button>
            <button data-demo-action="startAll">自动播放全部</button>
            <button data-demo-action="togglePause">${state.paused ? "继续" : "暂停"}</button>
            <button data-demo-action="step">单步下一条</button>
            <button data-demo-action="replay">重新播放</button>
            <button class="danger" data-demo-action="clear">清空并 idle</button>
          </div>
          <div class="os-demo-state"><span>${escapeHtml(state.status || "演示控制器尚未启动")}</span><div><i style="width:${Math.max(0, Math.min(100, progress))}%"></i></div><small>${Number(state.step_index || 0)} / ${Number(state.step_count || 0)}</small></div>
        </section>
        <section class="os-demo-log-card">
          <div><span>调试记录</span><b>输入 / 识别 / 触发 / AI / 解析 / 合并与丢弃</b></div>
          <pre>${escapeHtml(logs.length ? logs.join("\n") : "尚无演示记录")}</pre>
        </section>
        <section class="os-diagnostics-boundary">
          <b>演示隔离边界</b>
          <p>不写入正式档案、直播记录或生产统计；真实 AI 模式只调用既有本机演示入口，不修改模型、提示词或判断门槛。</p>
        </section>
      </div>
    </div>
  `;
}

function stageOsProcessingPage() {
  return `
    <div class="os-page os-processing-page">
      <div class="os-processing-orbit"><img src="${stageOsCoachAsset}" alt="叶灵正在整理本场" /></div>
      <div class="os-processing-copy">
        <div class="os-kicker">本场已安全结束</div>
        <h1>正在把直播事实整理成<br><span>可执行的下播承接。</span></h1>
        <p>只使用本场真实记录，不自动发送消息。</p>
      </div>
      <div class="os-processing-track"><i></i><i></i><i></i></div>
      <div class="os-processing-steps">
        <span class="done"><b>01</b><em>直播事实</em><small>观看、弹幕与礼物</small></span>
        <span class="active"><b>02</b><em>关系机会</em><small>识别可承接的人</small></span>
        <span><b>03</b><em>私信边界</em><small>生成能聊与不能聊</small></span>
      </div>
    </div>
  `;
}

function stageOsPostLivePage() {
  const state = stageOsPostLiveState;
  const cards = Array.isArray(state.cards) ? state.cards : [];
  const card = cards[stageOsPostLiveSelectedIndex] || null;
  const list = (values, empty = "暂无") => {
    const items = Array.isArray(values) ? values : [];
    return items.length
      ? `<ul>${items.map((value) => `<li>${escapeHtml(value)}</li>`).join("")}</ul>`
      : `<p class="os-postlive-empty">${empty}</p>`;
  };
  const outcomeLabel = (value) => ({
    sent: "已发送",
    replied: "有回复",
    no_reply: "未回复",
    do_not_contact: "不联系"
  }[value] || value || "—");

  if (stageOsCurrentRoute === "history") {
    const history = Array.isArray(state.history) ? state.history : [];
    return `
      <div class="os-page os-postlive-page os-postlive-workspace">
        <div class="os-workspace-title">
          <div><div class="os-kicker">历史报告</div><h1>承接结果记录</h1><p>这里只呈现本机已经记录的人工结果；系统不会自动发送消息。</p></div>
          <button data-postlive-source="latest">刷新</button>
        </div>
        <section class="os-postlive-history">
          <div class="os-postlive-history-head"><b>最近 ${history.length} 条</b><span>本机数据</span></div>
          ${history.length ? history.map((item) => `
            <article>
              <div><b>${escapeHtml(item.user_name || "未知观众")}</b><small>优先级 ${escapeHtml(item.priority || "—")}</small></div>
              <em>${escapeHtml(outcomeLabel(item.outcome))}</em>
              <time>${Number(item.timestamp_ms) > 0 ? new Date(Number(item.timestamp_ms)).toLocaleString("zh-CN", { hour12: false }) : "时间未知"}</time>
            </article>
          `).join("") : `<div class="os-postlive-blank"><b>还没有承接结果</b><p>在“观众关系”或“承接建议”中记录发送、回复或不联系结果后，会显示在这里。</p></div>`}
        </section>
      </div>
    `;
  }

  return `
    <div class="os-page os-postlive-page os-postlive-workspace">
      <div class="os-workspace-title">
        <div><div class="os-kicker">本场承接</div><h1>${stageOsCurrentRoute === "audience" ? "观众关系" : stageOsCurrentRoute === "followup" ? "承接建议" : "本场复盘"}</h1><p>先看事实，再决定要不要联系；系统不会自动发送任何消息。</p></div>
        <div class="os-postlive-toolbar">
          <span>${escapeHtml(state.source || "尚未读取")}</span>
          <button data-postlive-source="latest">刷新本场</button>
          ${stageOsCurrentRoute === "postlive" ? `<button data-postlive-source="simulation">加载模拟</button>` : ""}
        </div>
      </div>
      <div class="os-postlive-summary ${state.operation || "ready"}">
        <span>${stageOsPostLiveLoading ? "正在读取…" : escapeHtml(state.summary || "")}</span>
        ${state.message ? `<b>${escapeHtml(state.message)}</b>` : ""}
      </div>
      <div class="os-postlive-coach-grid">
        <aside class="os-postlive-candidates" aria-label="候选观众">
          <div><b>候选观众</b><span>${cards.length}</span></div>
          ${cards.length ? cards.map((item, index) => `
            <button class="${index === stageOsPostLiveSelectedIndex ? "active" : ""}" data-postlive-card="${index}">
              <span>${escapeHtml(item.user_name)}</span>
              <small>${escapeHtml(item.role)} · ${escapeHtml(item.relationship_level)}</small>
              <em class="${item.can_contact ? "" : "blocked"}">${item.can_contact ? `优先级 ${escapeHtml(item.priority)}` : "不联系"}</em>
            </button>
          `).join("") : `<div class="os-postlive-blank"><b>没有可显示的候选</b><p>需要本场真实弹幕或礼物证据达到既有阈值后才会生成。</p></div>`}
        </aside>
        <main class="os-postlive-detail">
          ${card ? `
            <header>
              <div><span>当前候选</span><h2>${escapeHtml(card.user_name)}</h2><p>优先级 ${escapeHtml(card.priority)} · 评分 ${Number(card.score || 0)} · ${escapeHtml(card.role)} · ${escapeHtml(card.relationship_level)} · 准备度 ${escapeHtml(card.readiness)}</p></div>
              <b class="${card.can_contact ? "allowed" : "blocked"}">${card.can_contact ? "可人工联系" : "禁止主动联系"}</b>
            </header>
            <div class="os-postlive-facts">
              <section><span>可确认事实</span><p>${escapeHtml(card.evidence_summary || "—")}</p></section>
              <section><span>缺失证据</span><p>${escapeHtml(card.missing_evidence || "—")}</p></section>
              <section><span>唯一目标</span><p>${escapeHtml(card.single_goal || "—")}</p></section>
              <section><span>时机与语气</span><p>${escapeHtml(card.timing || "—")}<br>${escapeHtml(card.tone || "—")}</p></section>
            </div>
            <section class="os-postlive-drafts">
              <div><span>开场草稿</span><b>${state.operation === "refined" ? "AI 草稿，发送前人工确认" : "本地安全模板"}</b></div>
              <div class="os-postlive-opener-list">
                ${(card.openers || []).map((opener, index) => `
                  <button class="${index === stageOsPostLiveSelectedOpener ? "active" : ""}" data-postlive-opener="${index}"><i>${index + 1}</i><span>${escapeHtml(opener)}</span></button>
                `).join("") || `<p class="os-postlive-empty">没有可用草稿</p>`}
              </div>
              <div class="os-postlive-actions">
                <button data-postlive-copy ${!card.can_contact || !(card.openers || []).length ? "disabled" : ""}>复制所选草稿</button>
                <button data-postlive-refine ${!card.can_contact || stageOsPostLiveLoading ? "disabled" : ""}>${stageOsPostLiveLoading ? "正在处理…" : "用豆包优化开场"}</button>
              </div>
            </section>
            <div class="os-postlive-guidance">
              <section><span>对方愿意展开</span>${list(card.if_warm_reply)}</section>
              <section><span>只简短回应</span>${list(card.if_short_reply)}</section>
              <section><span>没有回复</span>${list(card.if_no_reply)}</section>
              <section class="allowed"><span>允许话题</span>${list(card.allowed_topics)}</section>
              <section class="forbidden"><span>禁止话题</span>${list(card.forbidden_topics)}<b>停止条件</b>${list(card.stop_conditions)}</section>
            </div>
            <footer class="os-postlive-outcomes">
              <span>人工记录结果</span>
              <button data-postlive-outcome="sent">已发送</button>
              <button data-postlive-outcome="replied">有回复</button>
              <button data-postlive-outcome="no_reply">未回复</button>
              <button data-postlive-outcome="do_not_contact">不联系</button>
            </footer>
          ` : `
            <div class="os-postlive-blank os-postlive-blank-large">
              <b>暂无辅导卡</b>
              <p>系统只依据本场真实记录生成建议，不会用推测补齐观众动机。</p>
            </div>
          `}
        </main>
      </div>
    </div>
  `;
}

function stageOsEndModal() {
  if (!stageOsEndConfirmOpen) return "";
  return `
    <div class="os-modal-backdrop">
      <section class="os-end-dialog" role="dialog" aria-modal="true" aria-labelledby="end-title">
        <span>结束本场</span>
        <h2 id="end-title">确认结束直播教练？</h2>
        <p>会停止本场提醒并开始生成下播承接；直播平台本身仍由主播操作结束。</p>
        <div><button data-cancel-end>继续直播</button><button class="danger" data-confirm-end>确认结束</button></div>
      </section>
    </div>
  `;
}

function stageOsSafeExitModal() {
  if (!stageOsSafeExit) return "";
  return `
    <div class="os-modal-backdrop">
      <section class="os-end-dialog" role="dialog" aria-modal="true" aria-labelledby="safe-exit-title">
        <span>安全退出</span>
        <h2 id="safe-exit-title">${escapeHtml(stageOsSafeExit.title || "后台任务仍在进行")}</h2>
        <p>${escapeHtml(stageOsSafeExit.message || "请先完成或停止后台任务。")}</p>
        <div><button data-cancel-safe-exit>返回主程序</button><button class="danger" data-confirm-safe-exit>停止后台任务并退出</button></div>
      </section>
    </div>
  `;
}

function stageOsAccountLogoutModal() {
  if (!stageOsAccountLogoutConfirmOpen) return "";
  return `
    <div class="os-modal-backdrop">
      <section class="os-end-dialog" role="dialog" aria-modal="true" aria-labelledby="account-logout-title">
        <span>账号与会员</span>
        <h2 id="account-logout-title">确认退出并清除本机授权？</h2>
        <p>退出会停止本机托管 AI 会话。若没有离线保存恢复码，换机或重装后可能无法找回这个轻账号。</p>
        <div><button data-cancel-account-logout>返回账号页</button><button class="danger" data-confirm-account-logout>仍要退出</button></div>
      </section>
    </div>
  `;
}

function stageOsPadDeleteModal() {
  if (!stageOsPendingPadDeleteId) return "";
  return `
    <div class="os-modal-backdrop">
      <section class="os-end-dialog" role="dialog" aria-modal="true" aria-labelledby="pad-delete-title">
        <span>音效面板</span>
        <h2 id="pad-delete-title">从当前面板移除这个音效？</h2>
        <p>只移除当前面板中的条目，不会删除原始音频文件。</p>
        <div><button data-cancel-pad-delete>取消</button><button class="danger" data-confirm-pad-delete>确认移除</button></div>
      </section>
    </div>
  `;
}

// Re-render after the legacy script's first paint so the window never stays on
// the old multi-step shell.
activeStep = stageOsStepForPage(initialPage);
stageOsSessionPhase = activeStep === 8 ? "complete"
  : activeStep === 9 ? "processing"
  : "prep";
stageOsSessionStartedAt = null;
render();
post("navigateRoute", { route: stageOsCurrentRoute });
post("getSmartRackStatus");
post("getWeChatCollectorStatus");
