AI雨课堂助手(模块化构建版)

课堂习题提示,AI解答习题

// ==UserScript==
// @name         AI雨课堂助手(模块化构建版)
// @namespace    https://github.com/ZaytsevZY/yuketang-helper-auto
// @version      1.17.2-mod
// @description  课堂习题提示,AI解答习题
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=yuketang.cn
// @match        https://*.yuketang.cn/lesson/fullscreen/v3/*
// @match        https://*.yuketang.cn/v2/web/*
// @match        https://www.yuketang.cn/lesson/fullscreen/v3/*
// @match        https://www.yuketang.cn/v2/web/*
// @match        https://pro.yuketang.cn/lesson/fullscreen/v3/*
// @match        https://pro.yuketang.cn/v2/web/*
// @grant        GM_addStyle
// @grant        GM_notification
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @grant        GM_getTab
// @grant        GM_getTabs
// @grant        GM_saveTab
// @grant        unsafeWindow
// @run-at       document-start
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.umd.min.js
// ==/UserScript==
(function() {
  "use strict";
  // src/core/env.js
    const gm = {
    notify(opt) {
      if (typeof window.GM_notification === "function") window.GM_notification(opt);
    },
    addStyle(css) {
      if (typeof window.GM_addStyle === "function") window.GM_addStyle(css); else {
        const s = document.createElement("style");
        s.textContent = css;
        document.head.appendChild(s);
      }
    },
    xhr(opt) {
      if (typeof window.GM_xmlhttpRequest === "function") return window.GM_xmlhttpRequest(opt);
      throw new Error("GM_xmlhttpRequest is not available");
    },
    uw: window.unsafeWindow || window
  };
  function loadScriptOnce(src) {
    return new Promise((resolve, reject) => {
      if ([ ...document.scripts ].some(s => s.src === src)) return resolve();
      const s = document.createElement("script");
      s.src = src;
      s.onload = () => resolve();
      s.onerror = () => reject(new Error(`Failed to load: ${src}`));
      document.head.appendChild(s);
    });
  }
  async function ensureHtml2Canvas() {
    const w = gm.uw || window;
 // ★ 用页面 window
        if (typeof w.html2canvas === "function") return w.html2canvas;
    await loadScriptOnce("https://html2canvas.hertzen.com/dist/html2canvas.min.js");
    const h2c = w.html2canvas?.default || w.html2canvas;
    if (typeof h2c === "function") return h2c;
    throw new Error("html2canvas 未正确加载");
  }
  async function ensureJsPDF() {
    if (window.jspdf?.jsPDF) return window.jspdf;
    await loadScriptOnce("https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.umd.min.js");
    if (!window.jspdf?.jsPDF) throw new Error("jsPDF 未加载成功");
    return window.jspdf;
  }
  function randInt(l, r) {
    return l + Math.floor(Math.random() * (r - l + 1));
  }
  // src/core/types.js
    const PROBLEM_TYPE_MAP = {
    1: "单选题",
    2: "多选题",
    3: "投票题",
    4: "填空题",
    5: "主观题"
  };
  const DEFAULT_CONFIG = {
    notifyProblems: true,
    autoAnswer: false,
    autoAnswerDelay: 3e3,
    autoAnswerRandomDelay: 2e3,
    ai: {
      provider: "kimi",
      // ✅ 改为 kimi
      kimiApiKey: "",
      // ✅ 添加 kimi 专用字段
      apiKey: "",
      // 保持兼容
      endpoint: "https://api.moonshot.cn/v1/chat/completions",
      // ✅ Kimi API 端点
      model: "moonshot-v1-8k",
      // ✅ 文本模型
      visionModel: "moonshot-v1-8k-vision-preview",
      // ✅ 添加 Vision 模型配置
      temperature: .3,
      maxTokens: 1e3
    },
    showAllSlides: false,
    maxPresentations: 5
  };
  // src/core/storage.js
    class StorageManager {
    constructor(prefix) {
      this.prefix = prefix;
    }
    get(key, dv = null) {
      try {
        const v = localStorage.getItem(this.prefix + key);
        return v ? JSON.parse(v) : dv;
      } catch {
        return dv;
      }
    }
    set(key, value) {
      localStorage.setItem(this.prefix + key, JSON.stringify(value));
    }
    remove(key) {
      localStorage.removeItem(this.prefix + key);
    }
    getMap(key) {
      const arr = this.get(key, []);
      try {
        return new Map(arr);
      } catch {
        return new Map;
      }
    }
    setMap(key, map) {
      this.set(key, [ ...map ]);
    }
    alterMap(key, fn) {
      const m = this.getMap(key);
      fn(m);
      this.setMap(key, m);
    }
  }
  const storage = new StorageManager("ykt-helper:");
  // src/state/repo.js
    const repo = {
    presentations: new Map,
    // id -> presentation
    slides: new Map,
    // slideId -> slide
    problems: new Map,
    // problemId -> problem
    problemStatus: new Map,
    // problemId -> {presentationId, slideId, startTime, endTime, done, autoAnswerTime, answering}
    encounteredProblems: [],
    // [{problemId, ...ref}]
    currentPresentationId: null,
    currentSlideId: null,
    currentLessonId: null,
    // 1.16.4:按课程分组存储课件(presentations-<lessonId>)
    setPresentation(id, data) {
      this.presentations.set(id, {
        id: id,
        ...data
      });
      const key = this.currentLessonId ? `presentations-${this.currentLessonId}` : "presentations";
      storage.alterMap(key, m => {
        m.set(id, data);
        // 仍然做容量裁剪(向后兼容)
                const max = storage.get("config", {})?.maxPresentations ?? 5;
        const excess = m.size - max;
        if (excess > 0) [ ...m.keys() ].slice(0, excess).forEach(k => m.delete(k));
      });
    },
    upsertSlide(slide) {
      this.slides.set(slide.id, slide);
    },
    upsertProblem(prob) {
      this.problems.set(prob.problemId, prob);
    },
    pushEncounteredProblem(prob, slide, presentationId) {
      if (!this.encounteredProblems.some(p => p.problemId === prob.problemId)) this.encounteredProblems.push({
        problemId: prob.problemId,
        problemType: prob.problemType,
        body: prob.body || `题目ID: ${prob.problemId}`,
        options: prob.options || [],
        blanks: prob.blanks || [],
        answers: prob.answers || [],
        slide: slide,
        presentationId: presentationId
      });
    },
    // 1.16.4:载入本课(按课程分组)在本地存储过的课件
    loadStoredPresentations() {
      if (!this.currentLessonId) return;
      const key = `presentations-${this.currentLessonId}`;
      const stored = storage.getMap(key);
      for (const [id, data] of stored.entries()) this.setPresentation(id, data);
    }
  };
  // src/ui/toast.js
    function toast(message, duration = 2e3) {
    const el = document.createElement("div");
    el.textContent = message;
    el.style.cssText = `\n    position: fixed; top: 20px; left: 50%; transform: translateX(-50%);\n    background: rgba(0,0,0,.7); color: #fff; padding: 10px 20px;\n    border-radius: 4px; z-index: 10000000; max-width: 80%;\n  `;
    document.body.appendChild(el);
    setTimeout(() => {
      el.style.opacity = "0";
      el.style.transition = "opacity .5s";
      setTimeout(() => el.remove(), 500);
    }, duration);
  }
  var tpl$5 = '<div id="ykt-settings-panel" class="ykt-panel">\n  <div class="panel-header">\n    <h3>AI雨课堂助手设置</h3>\n    <span class="close-btn" id="ykt-settings-close"><i class="fas fa-times"></i></span>\n  </div>\n\n  <div class="panel-body">\n    <div class="settings-content">\n      <div class="setting-group">\n        <h4>AI配置</h4>\n          \x3c!-- 将DeepSeek相关配置替换为Kimi --\x3e\n          <div class="setting-item">\n              <label for="kimi-api-key">Kimi API Key:</label>\n              <input type="password" id="kimi-api-key" placeholder="输入您的 Kimi API Key">\n              <small>从 <a href="https://platform.moonshot.cn/" target="_blank">Kimi开放平台</a> 获取</small>\n          </div>\n      </div>\n\n      <div class="setting-group">\n        <h4>自动作答设置</h4>\n        <div class="setting-item">\n          <label class="checkbox-label">\n            <input type="checkbox" id="ykt-input-auto-answer">\n            <span class="checkmark"></span>\n            启用自动作答\n          </label>\n        </div>\n        <div class="setting-item">\n          <label class="checkbox-label">\n            <input type="checkbox" id="ykt-input-ai-auto-analyze">\n            <span class="checkmark"></span>\n            打开 AI 页面时自动分析\n          </label>\n          <small>开启后,进入“AI 解答”面板即自动向 AI 询问当前题目</small>\n        </div>\n        <div class="setting-item">\n          <label for="ykt-input-answer-delay">作答延迟时间 (秒):</label>\n          <input type="number" id="ykt-input-answer-delay" min="1" max="60">\n          <small>题目出现后等待多长时间开始作答</small>\n        </div>\n        <div class="setting-item">\n          <label for="ykt-input-random-delay">随机延迟范围 (秒):</label>\n          <input type="number" id="ykt-input-random-delay" min="0" max="30">\n          <small>在基础延迟基础上随机增加的时间范围</small>\n        </div>\n      </div>\n\n      <div class="setting-actions">\n        <button id="ykt-btn-settings-save">保存设置</button>\n        <button id="ykt-btn-settings-reset">重置为默认</button>\n      </div>\n    </div>\n  </div>\n</div>\n';
  let mounted$5 = false;
  let root$4;
  function mountSettingsPanel() {
    if (mounted$5) return root$4;
    root$4 = document.createElement("div");
    root$4.innerHTML = tpl$5;
    document.body.appendChild(root$4.firstElementChild);
    root$4 = document.getElementById("ykt-settings-panel");
    // 初始化表单
        const $api = root$4.querySelector("#kimi-api-key");
    const $auto = root$4.querySelector("#ykt-input-auto-answer");
    const $autoAnalyze = root$4.querySelector("#ykt-input-ai-auto-analyze");
    const $delay = root$4.querySelector("#ykt-input-answer-delay");
    const $rand = root$4.querySelector("#ykt-input-random-delay");
    $api.value = ui.config.ai.kimiApiKey || "";
    $auto.checked = !!ui.config.autoAnswer;
    $autoAnalyze.checked = !!ui.config.aiAutoAnalyze;
    $delay.value = Math.floor(ui.config.autoAnswerDelay / 1e3);
    $rand.value = Math.floor(ui.config.autoAnswerRandomDelay / 1e3);
    root$4.querySelector("#ykt-settings-close").addEventListener("click", () => showSettingsPanel(false));
    root$4.querySelector("#ykt-btn-settings-save").addEventListener("click", () => {
      ui.config.ai.kimiApiKey = $api.value.trim();
      ui.config.autoAnswer = !!$auto.checked;
      ui.config.aiAutoAnalyze = !!$autoAnalyze.checked;
      ui.config.autoAnswerDelay = Math.max(1e3, (+$delay.value || 0) * 1e3);
      ui.config.autoAnswerRandomDelay = Math.max(0, (+$rand.value || 0) * 1e3);
      storage.set("kimiApiKey", ui.config.ai.kimiApiKey);
      ui.saveConfig();
      ui.updateAutoAnswerBtn();
      ui.toast("设置已保存");
    });
    root$4.querySelector("#ykt-btn-settings-reset").addEventListener("click", () => {
      if (!confirm("确定要重置为默认设置吗?")) return;
      Object.assign(ui.config, DEFAULT_CONFIG);
      ui.config.ai.kimiApiKey = "";
      ui.config.aiAutoAnalyze = !!(DEFAULT_CONFIG.aiAutoAnalyze ?? false);
      storage.set("kimiApiKey", "");
      ui.saveConfig();
      ui.updateAutoAnswerBtn();
      $api.value = "";
      $auto.checked = DEFAULT_CONFIG.autoAnswer;
      $delay.value = Math.floor(DEFAULT_CONFIG.autoAnswerDelay / 1e3);
      $rand.value = Math.floor(DEFAULT_CONFIG.autoAnswerRandomDelay / 1e3);
      $autoAnalyze.checked = !!(DEFAULT_CONFIG.aiAutoAnalyze ?? false);
      ui.toast("设置已重置");
    });
    mounted$5 = true;
    return root$4;
  }
  function showSettingsPanel(visible = true) {
    mountSettingsPanel();
    const panel = document.getElementById("ykt-settings-panel");
    if (!panel) return;
    panel.classList.toggle("visible", !!visible);
  }
  function toggleSettingsPanel() {
    mountSettingsPanel();
    const panel = document.getElementById("ykt-settings-panel");
    showSettingsPanel(!panel.classList.contains("visible"));
  }
  var tpl$4 = '<div id="ykt-ai-answer-panel" class="ykt-panel">\n  <div class="panel-header">\n    <h3><i class="fas fa-robot"></i> AI 解答</h3>\n    <span id="ykt-ai-close" class="close-btn" title="关闭">\n      <i class="fas fa-times"></i>\n    </span>\n  </div>\n  <div class="panel-body">\n    <div class="ai-question-wrap">\n      <textarea id="ykt-ai-question-input" class="ai-question" placeholder="在此编辑要询问 AI 的题目…"></textarea>\n      <div class="ai-question-hint" style="font-size:12px;color:#666;margin-top:6px;">\n        提示:可直接编辑上面的题面再点击“向 AI 询问当前题目”或启用自动分析。\n      </div>\n    </div>\n    <div id="ykt-ai-loading" class="ai-loading" style="display: none;">\n      <i class="fas fa-spinner fa-spin"></i> AI正在分析...\n    </div>\n    <div id="ykt-ai-error" class="ai-error" style="display: none;"></div>\n    <div id="ykt-ai-answer" class="ai-answer"></div>\n    <div class="ai-actions">\n      <button id="ykt-ai-ask">向 AI 询问当前题目</button>\n      <button id="ykt-ai-ask-vision">Vision模式分析</button>\n    </div>\n  </div>\n</div>';
  // src/tsm/ai-format.js
    function formatProblemForAI(problem, TYPE_MAP) {
    let q = `请回答以下${TYPE_MAP[problem.problemType] || "题目"},按照格式回复:先给出答案,然后给出解释。\n\n题目:${problem.body || ""}`;
    if (problem.options?.length) {
      q += "\n选项:";
      for (const o of problem.options) q += `\n${o.key}. ${o.value}`;
    }
    q += `\n\n请按照以下格式回答:\n答案: [你的答案]\n解释: [详细解释]\n\n注意:\n- 单选题和投票题请回答选项字母\n- 多选题请回答多个选项字母\n- 填空题请直接给出答案内容\n- 主观题请给出完整回答`;
    return q;
  }
  function formatProblemForDisplay(problem, TYPE_MAP) {
    let s = `${TYPE_MAP[problem.problemType] || "题目"}:${problem.body || ""}`;
    if (problem.options?.length) {
      s += "\n\n选项:";
      for (const o of problem.options) s += `\n${o.key}. ${o.value}`;
    }
    return s;
  }
  function parseAIAnswer(problem, aiAnswer) {
    try {
      const lines = String(aiAnswer || "").split("\n");
      let answerLine = "";
      for (const line of lines) if (line.includes("答案:") || line.includes("答案:")) {
        answerLine = line.replace(/答案[::]\s*/, "").trim();
        break;
      }
      if (!answerLine) answerLine = lines[0]?.trim() || "";
      switch (problem.problemType) {
       case 1:
        break;

 // 单选
               case 3:
        {
          // 投票
          let m = answerLine.match(/[ABCD]/i);
          if (m) return [ m[0].toUpperCase() ];
          m = answerLine.match(/[A-Za-z]/);
          if (m) return [ m[0].toUpperCase() ];
          return null;
        }

       case 2:
        {
          // 多选
          let ms = answerLine.match(/[ABCD]/gi);
          if (ms?.length) return [ ...new Set(ms.map(x => x.toUpperCase())) ].sort();
          ms = answerLine.match(/[A-Za-z]/g);
          if (ms?.length) return [ ...new Set(ms.map(x => x.toUpperCase())) ].sort();
          return null;
        }

       case 4:
        {
          // 填空
          const blanks = answerLine.split(/[,,\s]+/).filter(Boolean);
          return blanks.length ? blanks : null;
        }

       case 5:
        // 主观
        return {
          content: answerLine,
          pics: []
        };

       default:
        return null;
      }
    } catch (e) {
      console.error("[parseAIAnswer] failed", e);
      return null;
    }
  }
  // src/ai/kimi.js
  /**
   * 调用 Kimi 文本模型
   * @param {string} question 题目内容
   * @param {Object} aiCfg AI配置
   * @returns {Promise<string>} AI回答
   */  async function queryKimi(question, aiCfg) {
    const apiKey = aiCfg.kimiApiKey;
    if (!apiKey) throw new Error("请先配置 Kimi API Key");
    return new Promise((resolve, reject) => {
      gm.xhr({
        method: "POST",
        url: "https://api.moonshot.cn/v1/chat/completions",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`
        },
        data: JSON.stringify({
          model: "moonshot-v1-8k",
          // ✅ 文本模型
          messages: [ {
            role: "system",
            content: "你是 Kimi,由 Moonshot AI 提供的人工智能助手。请简洁准确地回答用户的问题,特别是选择题请直接给出答案选项。"
          }, {
            role: "user",
            content: question
          } ],
          temperature: .6
        }),
        onload: res => {
          try {
            console.log("[Kimi API] Status:", res.status);
            console.log("[Kimi API] Response:", res.responseText);
            if (res.status !== 200) {
              reject(new Error(`Kimi API 请求失败: ${res.status}`));
              return;
            }
            const data = JSON.parse(res.responseText);
            const content = data.choices?.[0]?.message?.content;
            if (content) resolve(content); else reject(new Error("AI返回内容为空"));
          } catch (e) {
            reject(new Error(`解析API响应失败: ${e.message}`));
          }
        },
        onerror: () => reject(new Error("网络请求失败")),
        timeout: 3e4
      });
    });
  }
  /**
   * 调用 Kimi Vision模型(图像+文本)
   * @param {string} imageBase64 图像的base64编码
   * @param {string} textPrompt 文本提示(可包含题干)
   * @param {Object} aiCfg AI配置
   * @returns {Promise<string>} AI回答
   */  async function queryKimiVision(imageBase64, textPrompt, aiCfg) {
    const apiKey = aiCfg.kimiApiKey;
    if (!apiKey) throw new Error("请先配置 Kimi API Key");
    // ✅ 检查图像数据格式
        if (!imageBase64 || typeof imageBase64 !== "string") throw new Error("图像数据格式错误");
    // ✅ 确保 base64 数据格式正确
        const cleanBase64 = imageBase64.replace(/^data:image\/[^;]+;base64,/, "");
    // ✅ 按照文档要求构建消息格式
        const messages = [ {
      role: "system",
      content: "你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。"
    }, {
      role: "user",
      content: [ {
        type: "image_url",
        image_url: {
          url: `data:image/png;base64,${cleanBase64}`
        }
      }, {
        type: "text",
        text: textPrompt || "请分析图片中的题目并给出答案"
      } ]
    } ];
    return new Promise((resolve, reject) => {
      console.log("[Kimi Vision] 发送请求...");
      console.log("[Kimi Vision] 模型: moonshot-v1-8k-vision-preview");
      console.log("[Kimi Vision] 图片数据长度:", cleanBase64.length);
      gm.xhr({
        method: "POST",
        url: "https://api.moonshot.cn/v1/chat/completions",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${apiKey}`
        },
        data: JSON.stringify({
          model: "moonshot-v1-8k-vision-preview",
          // ✅ 使用 Vision 专用模型
          messages: messages,
          temperature: .3
        }),
        onload: res => {
          try {
            console.log("[Kimi Vision] Status:", res.status);
            console.log("[Kimi Vision] Response:", res.responseText);
            if (res.status !== 200) {
              // ✅ 提供更详细的错误信息
              let errorMessage = `Kimi Vision API 请求失败: ${res.status}`;
              try {
                const errorData = JSON.parse(res.responseText);
                if (errorData.error?.message) errorMessage += ` - ${errorData.error.message}`;
                if (errorData.error?.code) errorMessage += ` (${errorData.error.code})`;
              } catch (e) {
                errorMessage += ` - ${res.responseText}`;
              }
              reject(new Error(errorMessage));
              return;
            }
            const data = JSON.parse(res.responseText);
            const content = data.choices?.[0]?.message?.content;
            if (content) {
              console.log("[Kimi Vision] 成功获取回答");
              resolve(content);
            } else reject(new Error("AI返回内容为空"));
          } catch (e) {
            console.error("[Kimi Vision] 解析响应失败:", e);
            reject(new Error(`解析API响应失败: ${e.message}`));
          }
        },
        onerror: err => {
          console.error("[Kimi Vision] 网络请求失败:", err);
          reject(new Error("网络请求失败"));
        },
        timeout: 6e4
      });
    });
  }
  var kimi =  Object.freeze({
    __proto__: null,
    queryKimi: queryKimi,
    queryKimiVision: queryKimiVision
  });
  // src/tsm/answer.js
  // Refactored from v1.16.1 userscript to module style.
  // Exposes three primary APIs:
  //   - answerProblem(problem, result, options)
  //   - retryAnswer(problem, result, dt, options)
  //   - submitAnswer(problem, result, submitOptions)  // orchestrates answer vs retry
  
  // Differences vs userscript:
  // - No global UI (confirm/Toast). Callers control UX.
  // - Uses options to pass deadline window and behavior flags.
  // - Allows header overrides for testing and non-browser envs.
    const DEFAULT_HEADERS = () => ({
    "Content-Type": "application/json",
    xtbz: "ykt",
    "X-Client": "h5",
    Authorization: "Bearer " + (typeof localStorage !== "undefined" ? localStorage.getItem("Authorization") : "")
  });
  /**
   * Low-level POST helper using XMLHttpRequest to align with site requirements.
   * @param {string} url
   * @param {object} data
   * @param {Record<string,string>} headers
   * @returns {Promise<any>}
   */  function xhrPost(url, data, headers) {
    return new Promise((resolve, reject) => {
      try {
        const xhr = new XMLHttpRequest;
        xhr.open("POST", url);
        for (const [k, v] of Object.entries(headers || {})) xhr.setRequestHeader(k, v);
        xhr.onload = () => {
          try {
            const resp = JSON.parse(xhr.responseText);
            if (resp && typeof resp === "object") resolve(resp); else reject(new Error("解析响应失败"));
          } catch {
            reject(new Error("解析响应失败"));
          }
        };
        xhr.onerror = () => reject(new Error("网络请求失败"));
        xhr.send(JSON.stringify(data));
      } catch (e) {
        reject(e);
      }
    });
  }
  /**
   * POST /api/v3/lesson/problem/answer
   * Mirrors the 1.16.1 logic (no UI). Returns {code, data, msg, ...} on success code===0.
   * @param {{problemId:number, problemType:number}} problem
   * @param {any} result
   * @param {{headers?:Record<string,string>, dt?:number}} [options]
   */  async function answerProblem(problem, result, options = {}) {
    const url = "/api/v3/lesson/problem/answer";
    const headers = {
      ...DEFAULT_HEADERS(),
      ...options.headers || {}
    };
    const payload = {
      problemId: problem.problemId,
      problemType: problem.problemType,
      dt: options.dt ?? Date.now(),
      result: result
    };
    const resp = await xhrPost(url, payload, headers);
    if (resp.code === 0) return resp;
    throw new Error(`${resp.msg} (${resp.code})`);
  }
  /**
   * POST /api/v3/lesson/problem/retry
   * Expects server to echo success ids in data.success (as in v1.16.1).
   * @param {{problemId:number, problemType:number}} problem
   * @param {any} result
   * @param {number} dt - simulated answer time (epoch ms)
   * @param {{headers?:Record<string,string>}} [options]
   */  async function retryAnswer(problem, result, dt, options = {}) {
    const url = "/api/v3/lesson/problem/retry";
    const headers = {
      ...DEFAULT_HEADERS(),
      ...options.headers || {}
    };
    const payload = {
      problems: [ {
        problemId: problem.problemId,
        problemType: problem.problemType,
        dt: dt,
        result: result
      } ]
    };
    const resp = await xhrPost(url, payload, headers);
    if (resp.code !== 0) throw new Error(`${resp.msg} (${resp.code})`);
    const okList = resp?.data?.success || [];
    if (!Array.isArray(okList) || !okList.includes(problem.problemId)) throw new Error("服务器未返回成功信息");
    return resp;
  }
  /**
   * High-level orchestrator: answer first; if deadline has passed, optionally retry.
   * This is the module adaptation of the 1.16.1 userscript submit flow.
   *
   * @param {{problemId:number, problemType:number}} problem
   * @param {any} result
   * @param {Object} submitOptions
   * @param {number} [submitOptions.startTime] - unlock time (epoch ms). Required for retry path.
   * @param {number} [submitOptions.endTime]   - deadline (epoch ms). If now >= endTime -> retry path.
   * @param {boolean} [submitOptions.forceRetry=false] - when past deadline, directly use retry without prompting.
   * @param {number} [submitOptions.retryDtOffsetMs=2000] - dt = startTime + offset when retrying.
   * @param {Record<string,string>} [submitOptions.headers] - extra/override headers.
   * @returns {Promise<{'route':'answer'|'retry', resp:any}>}
   */  async function submitAnswer(problem, result, submitOptions = {}) {
    const {startTime: startTime, endTime: endTime, forceRetry: forceRetry = false, retryDtOffsetMs: retryDtOffsetMs = 2e3, headers: headers} = submitOptions;
    const now = Date.now();
    const pastDeadline = typeof endTime === "number" && now >= endTime;
    if (pastDeadline) {
      if (!forceRetry) {
        const err = new Error("DEADLINE_PASSED");
        err.name = "DeadlineError";
        err.details = {
          startTime: startTime,
          endTime: endTime,
          now: now
        };
        throw err;
      }
      const base = typeof startTime === "number" ? startTime : now - retryDtOffsetMs;
      const dt = base + retryDtOffsetMs;
      const resp = await retryAnswer(problem, result, dt, {
        headers: headers
      });
      return {
        route: "retry",
        resp: resp
      };
    }
    const resp = await answerProblem(problem, result, {
      headers: headers,
      dt: now
    });
    return {
      route: "answer",
      resp: resp
    };
  }
  // src/ui/auto-answer-popup.js
  // 简单 HTML 转义,避免把题目中的 <> 等插入为标签
    function esc(s) {
    return String(s).replace(/[&<>"']/g, c => ({
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;",
      '"': "&quot;",
      "'": "&#39;"
    }[c]));
  }
  /**
   * 显示自动作答成功弹窗
   * @param {object} problem - 题目对象
   * @param {string} aiAnswer - 原始 AI 文本(未解析前)
   * @param {object} [cfg] - 可选配置(用于局部覆写)
   */  function showAutoAnswerPopup(problem, aiAnswer, cfg = {}) {
    // 避免重复
    const existed = document.getElementById("ykt-auto-answer-popup");
    if (existed) existed.remove();
    const popup = document.createElement("div");
    popup.id = "ykt-auto-answer-popup";
    popup.className = "auto-answer-popup";
    // 模块版签名:需要传 TYPE_MAP
        const questionText = formatProblemForDisplay(problem, ui.config && ui.config.TYPE_MAP || {});
    // 采用“全屏遮罩 + 内部卡片”的结构,外层用于点击关闭
        popup.innerHTML = `\n    <div class="popup-content">\n      <div class="popup-header">\n        <h4><i class="fas fa-robot"></i> AI自动作答成功</h4>\n        <span class="close-btn" title="关闭"><i class="fas fa-times"></i></span>\n      </div>\n      <div class="popup-body">\n        <div class="popup-row popup-question">\n          <div class="label">题目:</div>\n          <div class="content">${esc(questionText).replace(/\n/g, "<br>")}</div>\n        </div>\n        <div class="popup-row popup-answer">\n          <div class="label">AI回答:</div>\n          <div class="content">${esc(aiAnswer || "").replace(/\n/g, "<br>")}</div>\n        </div>\n      </div>\n    </div>\n  `;
    document.body.appendChild(popup);
    // 关闭按钮
        popup.querySelector(".close-btn")?.addEventListener("click", () => popup.remove());
    // 点击遮罩关闭(只在点击外层时才关闭)
        popup.addEventListener("click", e => {
      if (e.target === popup) popup.remove();
    });
    // 自动关闭
        const ac = ui.config && ui.config.autoAnswerPopup || {};
    const autoClose = cfg.autoClose ?? ac.autoClose ?? true;
    const autoDelay = cfg.autoCloseDelay ?? ac.autoCloseDelay ?? 4e3;
    if (autoClose) setTimeout(() => {
      if (popup.parentNode) popup.remove();
    }, autoDelay);
    // 入场动画
        requestAnimationFrame(() => popup.classList.add("visible"));
  }
  // src/capture/screenshot.js
    async function captureProblemScreenshot() {
    try {
      const html2canvas = await ensureHtml2Canvas();
      const el = document.querySelector(".ques-title") || document.querySelector(".problem-body") || document.querySelector(".ppt-inner") || document.querySelector(".ppt-courseware-inner") || document.body;
      return await html2canvas(el, {
        useCORS: true,
        allowTaint: false,
        backgroundColor: "#ffffff",
        // ✅ 设置背景色
        scale: 1,
        // ✅ 设置缩放比例
        width: Math.min(el.scrollWidth, 1200),
        // ✅ 限制宽度
        height: Math.min(el.scrollHeight, 800)
      });
    } catch (e) {
      console.error("[captureProblemScreenshot] failed", e);
      return null;
    }
  }
  // 新增:获取问题页面截图的base64数据,供Vision API使用
    async function captureProblemForVision() {
    try {
      console.log("[captureProblemForVision] 开始截图...");
      const canvas = await captureProblemScreenshot();
      if (!canvas) {
        console.error("[captureProblemForVision] 截图失败");
        return null;
      }
      console.log("[captureProblemForVision] 截图成功,转换为base64...");
      // ✅ 转换为 JPEG 格式以减小文件大小
            const base64 = canvas.toDataURL("image/jpeg", .8).split(",")[1];
      console.log("[captureProblemForVision] base64 长度:", base64.length);
      // ✅ 检查图片大小,如果太大则压缩
            if (base64.length > 1e6) {
        // 1MB
        console.log("[captureProblemForVision] 图片过大,进行压缩...");
        // 重新生成更小的图片
                const smallerBase64 = canvas.toDataURL("image/jpeg", .5).split(",")[1];
        console.log("[captureProblemForVision] 压缩后长度:", smallerBase64.length);
        return smallerBase64;
      }
      return base64;
    } catch (e) {
      console.error("[captureProblemForVision] failed", e);
      return null;
    }
  }
  var screenshoot =  Object.freeze({
    __proto__: null,
    captureProblemForVision: captureProblemForVision,
    captureProblemScreenshot: captureProblemScreenshot
  });
  let mounted$4 = false;
  let root$3;
  function $$4(sel) {
    return document.querySelector(sel);
  }
  function mountAIPanel() {
    if (mounted$4) return root$3;
    const host = document.createElement("div");
    host.innerHTML = tpl$4;
    document.body.appendChild(host.firstElementChild);
    root$3 = document.getElementById("ykt-ai-answer-panel");
    // 关闭面板
        $$4("#ykt-ai-close")?.addEventListener("click", () => showAIPanel(false));
    // 手动点击按钮触发 AI 分析
        $$4("#ykt-ai-ask")?.addEventListener("click", askAIForCurrent);
    // Vision 模式按钮
        $$4("#ykt-ai-ask-vision")?.addEventListener("click", askAIVisionForCurrent);
    mounted$4 = true;
    return root$3;
  }
  window.addEventListener("ykt:open-ai", () => {
    showAIPanel(true);
 // 打开面板
    });
  function showAIPanel(visible = true) {
    mountAIPanel();
    root$3.classList.toggle("visible", !!visible);
    if (visible) {
      renderQuestion();
      // 自动分析:只有开关打开时才调用
            if (ui.config.aiAutoAnalyze) queueMicrotask(() => {
        askAIForCurrent();
      });
    }
    // 同步工具栏按钮状态
        const aiBtn = document.getElementById("ykt-btn-ai");
    if (aiBtn) aiBtn.classList.toggle("active", !!visible);
  }
  function setAILoading(v) {
    mountAIPanel();
    $$4("#ykt-ai-loading").style.display = v ? "" : "none";
  }
  function setAIError(msg = "") {
    mountAIPanel();
    const el = $$4("#ykt-ai-error");
    el.style.display = msg ? "" : "none";
    el.textContent = msg || "";
  }
  function setAIAnswer(content = "") {
    mountAIPanel();
    $$4("#ykt-ai-answer").textContent = content || "";
  }
  function renderQuestion() {
    const p = repo.currentSlideId ? repo.slides.get(repo.currentSlideId)?.problem : null;
    const problem = p || (repo.encounteredProblems.at(-1) ? repo.problems.get(repo.encounteredProblems.at(-1).problemId) : null);
    const text = problem ? formatProblemForDisplay(problem, ui.config.TYPE_MAP || {}) : "未选择题目";
    const el = document.querySelector("#ykt-ai-question-input");
    if (el) 
    // 若用户已经编辑过则不覆盖;首次为空时才灌入默认题面
    if (!el.value.trim()) el.value = text;
  }
  function getEditedQuestion() {
    const el = document.querySelector("#ykt-ai-question-input");
    const v = el ? el.value.trim() : "";
    return v;
  }
  // 新增:使用Vision模式询问AI
  // 在 askAIVisionForCurrent 函数中添加更多调试信息
    async function askAIVisionForCurrent() {
    const slide = repo.currentSlideId ? repo.slides.get(repo.currentSlideId) : null;
    const problem = slide?.problem || (repo.encounteredProblems.at(-1) ? repo.problems.get(repo.encounteredProblems.at(-1).problemId) : null);
    setAIError("");
    setAILoading(true);
    setAIAnswer("");
    try {
      // 1. 检查 API Key
      if (!ui.config.ai.kimiApiKey) throw new Error("请先在设置中配置 Kimi API Key");
      // 2. 截取当前页面图像
            ui.toast("正在截取页面图像...", 2e3);
      console.log("[Vision] 开始截图...");
      const imageBase64 = await captureProblemForVision();
      if (!imageBase64) throw new Error("无法截取页面图像,请确保页面内容已加载完成");
      console.log("[Vision] 截图完成,图像大小:", imageBase64.length);
      // 3. 准备文本提示
            const edited = getEditedQuestion();
      let textPrompt = edited && edited.length > 0 ? `请分析图片并结合以下用户输入的题目信息作答。用户输入的题目信息是:\n\n${edited}\n\n请按“答案/解释”的格式返回。` : "请分析图片中的题目并给出答案。按照以下格式回答:\n答案: [你的答案]\n解释: [详细解释]";
      if (!edited && problem && problem.body) {
        const problemText = formatProblemForAI(problem, ui.config.TYPE_MAP || {});
        textPrompt = `请结合以下题目信息分析图片:\n\n${problemText}\n\n请仔细观察图片内容,给出准确答案。`;
      }
      // 4. 调用Vision API
            ui.toast("正在使用Vision模式分析...", 3e3);
      console.log("[Vision] 调用API...");
      const aiContent = await queryKimiVision(imageBase64, textPrompt, ui.config.ai);
      setAILoading(false);
      console.log("[Vision] API调用成功");
      setAIAnswer(`Vision模式回答:\n${aiContent}`);
      // 5. 如果有题目对象,尝试解析答案并提供提交按钮
            if (problem) {
        const parsed = parseAIAnswer(problem, aiContent);
        if (parsed) {
          setAIAnswer(`Vision模式回答:\n${aiContent}\n\nAI 建议答案:${JSON.stringify(parsed)}`);
          const submitBtn = document.createElement("button");
          submitBtn.textContent = "提交答案";
          submitBtn.onclick = async () => {
            try {
              await submitAnswer(problem, parsed);
              ui.toast("提交成功");
              showAutoAnswerPopup(problem, aiContent);
            } catch (e) {
              ui.toast(`提交失败: ${e.message}`);
            }
          };
          $$4("#ykt-ai-answer").appendChild(document.createElement("br"));
          $$4("#ykt-ai-answer").appendChild(submitBtn);
        } else setAIAnswer(`Vision模式回答:\n${aiContent}\n\n注意:无法自动解析答案格式,请手动查看上述回答。`);
      }
    } catch (e) {
      setAILoading(false);
      console.error("[Vision] 完整错误信息:", e);
      // ✅ 提供降级建议
            let errorMsg = `Vision模式失败: ${e.message}`;
      if (e.message.includes("400")) errorMsg += "\n\n可能的解决方案:\n1. 检查 API Key 是否正确\n2. 尝试刷新页面后重试\n3. 使用普通文本模式";
      setAIError(errorMsg);
    }
  }
  // 修改原有的askAIForCurrent,保持兼容
    async function askAIForCurrent() {
    const slide = repo.currentSlideId ? repo.slides.get(repo.currentSlideId) : null;
    const problem = slide?.problem || (repo.encounteredProblems.at(-1) ? repo.problems.get(repo.encounteredProblems.at(-1).problemId) : null);
    const edited = getEditedQuestion();
 // ← 读取用户编辑内容
    // 若没有题面文本(用户也没编辑)且无法拿到 problem,则自动切到 Vision 模式
        if (!edited && (!problem || !problem.body)) {
      ui.toast("未检测到题目文本,自动使用Vision模式", 2e3);
      return askAIVisionForCurrent();
    }
    setAIError("");
    setAILoading(true);
    setAIAnswer("");
    try {
      // 1) 构造要问 AI 的文本:优先使用“用户编辑”的题面
      const q = edited || formatProblemForAI(problem, ui.config.TYPE_MAP || {});
      // 2) 请求
            const aiContent = await queryKimi(q, ui.config.ai);
      // 3) 若有 problem,尝试解析并提供“提交答案”
            let parsed = null;
      if (problem) parsed = parseAIAnswer(problem, aiContent);
      setAILoading(false);
      if (parsed) {
        setAIAnswer(`AI 建议答案:${JSON.stringify(parsed)}`);
        const submitBtn = document.createElement("button");
        submitBtn.textContent = "提交答案";
        submitBtn.onclick = async () => {
          try {
            await submitAnswer(problem, parsed);
            ui.toast("提交成功");
            showAutoAnswerPopup(problem, typeof aiContent === "string" ? aiContent : JSON.stringify(aiContent, null, 2));
          } catch (e) {
            ui.toast(`提交失败: ${e.message}`);
          }
        };
        document.querySelector("#ykt-ai-answer").appendChild(document.createElement("br"));
        document.querySelector("#ykt-ai-answer").appendChild(submitBtn);
      } else 
      // 无法解析就直接把原文显示给用户
      setAIAnswer(typeof aiContent === "string" ? aiContent : JSON.stringify(aiContent, null, 2));
    } catch (e) {
      setAILoading(false);
      setAIError(e.message);
    }
  }
  var tpl$3 = '<div id="ykt-presentation-panel" class="ykt-panel">\n  <div class="panel-header">\n    <h3>课件浏览</h3>\n    <div class="panel-controls">\n      <label>\n        <input type="checkbox" id="ykt-show-all-slides"> 切换全部页面/问题页面\n      </label>\n      <button id="ykt-open-problem-list">题目列表</button>\n      <button id="ykt-download-current">截图下载</button>\n      <button id="ykt-download-pdf">整册下载(PDF)</button>\n      <span class="close-btn" id="ykt-presentation-close"><i class="fas fa-times"></i></span>\n    </div>\n  </div>\n\n  <div class="panel-body">\n    <div class="panel-left">\n      <div id="ykt-presentation-list" class="presentation-list"></div>\n    </div>\n    <div class="panel-right">\n      <div id="ykt-slide-view" class="slide-view">\n        <div class="slide-cover">\n          <div class="empty-message">选择左侧的幻灯片查看详情</div>\n        </div>\n        <div id="ykt-problem-view" class="problem-view"></div>\n      </div>\n    </div>\n  </div>\n</div>\n';
  let mounted$3 = false;
  let host;
  function $$3(sel) {
    return document.querySelector(sel);
  }
  function mountPresentationPanel() {
    if (mounted$3) return host;
    const wrapper = document.createElement("div");
    wrapper.innerHTML = tpl$3;
    document.body.appendChild(wrapper.firstElementChild);
    host = document.getElementById("ykt-presentation-panel");
    $$3("#ykt-presentation-close")?.addEventListener("click", () => showPresentationPanel(false));
    $$3("#ykt-open-problem-list")?.addEventListener("click", () => {
      showPresentationPanel(false);
      window.dispatchEvent(new CustomEvent("ykt:open-problem-list"));
    });
    $$3("#ykt-download-current")?.addEventListener("click", downloadCurrentSlide);
    $$3("#ykt-download-pdf")?.addEventListener("click", downloadPresentationPDF);
    const cb = $$3("#ykt-show-all-slides");
    cb.checked = !!ui.config.showAllSlides;
    cb.addEventListener("change", () => {
      ui.config.showAllSlides = !!cb.checked;
      ui.saveConfig();
      updatePresentationList();
    });
    mounted$3 = true;
    return host;
  }
  // 在 showPresentationPanel 函数中添加按钮状态同步
    function showPresentationPanel(visible = true) {
    mountPresentationPanel();
    host.classList.toggle("visible", !!visible);
    if (visible) updatePresentationList();
    // 同步工具栏按钮状态
        const presBtn = document.getElementById("ykt-btn-pres");
    if (presBtn) presBtn.classList.toggle("active", !!visible);
  }
  // export function updatePresentationList() {
  //   mountPresentationPanel();
  //   const list = $('#ykt-presentation-list');
  //   list.innerHTML = '';
  //   const showAll = !!ui.config.showAllSlides;
  //   const presEntries = [...repo.presentations.values()].slice(-ui.config.maxPresentations);
  //   presEntries.forEach((pres) => {
  //     const item = document.createElement('div');
  //     item.className = 'presentation-item';
  //     const title = document.createElement('div');
  //     title.className = 'presentation-title';
  //     title.textContent = pres.title || `课件 ${pres.id}`;
  //     item.appendChild(title);
  //     const slidesWrap = document.createElement('div');
  //     slidesWrap.className = 'slide-thumb-list';
  //     (pres.slides || []).forEach((s) => {
  //       if (!showAll && !s.problem) return;
  //       const thumb = document.createElement('div');
  //       thumb.className = 'slide-thumb';
  //       thumb.title = s.title || `第 ${s.page} 页`;
  //       if (s.thumbnail) {
  //         const img = document.createElement('img');
  //         img.src = s.thumbnail;
  //         img.alt = thumb.title;
  //         thumb.appendChild(img);
  //       } else {
  //         thumb.textContent = s.title || String(s.page ?? '');
  //       }
  //       thumb.addEventListener('click', () => {
  //         repo.currentPresentationId = pres.id;
  //         repo.currentSlideId = s.id;
  //         updateSlideView();
  //       });
  //       slidesWrap.appendChild(thumb);
  //     });
  //     item.appendChild(slidesWrap);
  //     list.appendChild(item);
  //   });
  // }
  //1.16.4 更新课件加载方法
    function updatePresentationList() {
    mountPresentationPanel();
    const listEl = document.getElementById("ykt-presentation-list");
    if (!listEl) return;
    listEl.innerHTML = "";
    if (repo.presentations.size === 0) {
      listEl.innerHTML = '<p class="no-presentations">暂无课件记录</p>';
      return;
    }
    // 只显示当前课程的课件(基于 URL 与 repo.currentLessonId 过滤)
        const currentPath = window.location.pathname;
    const m = currentPath.match(/\/lesson\/fullscreen\/v3\/([^/]+)/);
    const currentLessonFromURL = m ? m[1] : null;
    const filtered = new Map;
    for (const [id, presentation] of repo.presentations) 
    // 若 URL 和 repo 同时能取到 lessonId,则要求一致
    if (currentLessonFromURL && repo.currentLessonId && currentLessonFromURL === repo.currentLessonId) filtered.set(id, presentation); else if (!currentLessonFromURL) 
    // 向后兼容:无法从 URL 提取课程 ID 时,展示全部
    filtered.set(id, presentation); else if (currentLessonFromURL === repo.currentLessonId) filtered.set(id, presentation);
    const presentationsToShow = filtered.size > 0 ? filtered : repo.presentations;
    for (const [id, presentation] of presentationsToShow) {
      const cont = document.createElement("div");
      cont.className = "presentation-container";
      // 标题 + 下载按钮
            const titleEl = document.createElement("div");
      titleEl.className = "presentation-title";
      titleEl.innerHTML = `\n      <span>${presentation.title || `课件 ${id}`}</span>\n      <i class="fas fa-download download-btn" title="下载课件"></i>\n    `;
      cont.appendChild(titleEl);
      // 下载按钮
            titleEl.querySelector(".download-btn")?.addEventListener("click", e => {
        e.stopPropagation();
        downloadPresentation(presentation);
      });
      // 幻灯片缩略图区域
            const slidesWrap = document.createElement("div");
      slidesWrap.className = "slide-thumb-list";
      // 是否显示全部页
            const showAll = !!ui.config.showAllSlides;
      const slidesToShow = showAll ? presentation.slides || [] : (presentation.slides || []).filter(s => s.problem);
      for (const s of slidesToShow) {
        const thumb = document.createElement("div");
        thumb.className = "slide-thumb";
        // 当前高亮
                if (s.id === repo.currentSlideId) thumb.classList.add("active");
        // 状态样式:解锁 / 已作答
                if (s.problem) {
          const pid = s.problem.problemId;
          const status = repo.problemStatus.get(pid);
          if (status) thumb.classList.add("unlocked");
          if (s.problem.result) thumb.classList.add("answered");
        }
        // 点击跳转
                thumb.addEventListener("click", () => {
          actions.navigateTo(presentation.id, s.id);
        });
        // 缩略图内容
                const img = document.createElement("img");
        if (presentation.width && presentation.height) img.style.aspectRatio = `${presentation.width}/${presentation.height}`;
        img.src = s.thumbnail || "";
        img.alt = s.title || `第 ${s.page ?? ""} 页`;
        // 关键:图片加载失败时移除(可能非本章节的页)
                img.onerror = function() {
          if (thumb.parentNode) thumb.parentNode.removeChild(thumb);
        };
        const idx = document.createElement("span");
        idx.className = "slide-index";
        idx.textContent = s.index ?? "";
        thumb.appendChild(img);
        thumb.appendChild(idx);
        slidesWrap.appendChild(thumb);
      }
      cont.appendChild(slidesWrap);
      listEl.appendChild(cont);
    }
  }
  // 课件下载入口:切换当前课件后调用现有 PDF 导出逻辑
    function downloadPresentation(presentation) {
    // 先切到该课件,再复用“整册下载(PDF)”按钮逻辑
    repo.currentPresentationId = presentation.id;
    // 这里直接调用现有的 downloadPresentationPDF(定义在本文件尾部)
    // 若你希望仅下载题目页,可根据 ui.config.showAllSlides 控制
        downloadPresentationPDF();
  }
  function updateSlideView() {
    mountPresentationPanel();
    const slideView = $$3("#ykt-slide-view");
    const problemView = $$3("#ykt-problem-view");
    slideView.querySelector(".slide-cover")?.classList.add("hidden");
    problemView.innerHTML = "";
    if (!repo.currentSlideId) {
      slideView.querySelector(".slide-cover")?.classList.remove("hidden");
      return;
    }
    const slide = repo.slides.get(repo.currentSlideId);
    if (!slide) return;
    const cover = document.createElement("div");
    cover.className = "slide-cover";
    const img = document.createElement("img");
    img.crossOrigin = "anonymous";
    img.src = slide.image || slide.thumbnail || "";
    img.alt = slide.title || "";
    cover.appendChild(img);
    if (slide.problem) {
      const prob = slide.problem;
      const box = document.createElement("div");
      box.className = "problem-box";
      const head = document.createElement("div");
      head.className = "problem-head";
      head.textContent = prob.body || `题目 ${prob.problemId}`;
      box.appendChild(head);
      if (Array.isArray(prob.options) && prob.options.length) {
        const opts = document.createElement("div");
        opts.className = "problem-options";
        prob.options.forEach(o => {
          const li = document.createElement("div");
          li.className = "problem-option";
          li.textContent = `${o.key}. ${o.value}`;
          opts.appendChild(li);
        });
        box.appendChild(opts);
      }
      problemView.appendChild(box);
    }
    slideView.innerHTML = "";
    slideView.appendChild(cover);
    slideView.appendChild(problemView);
  }
  async function downloadCurrentSlide() {
    if (!repo.currentSlideId) return ui.toast("请先选择一页课件/题目");
    const slide = repo.slides.get(repo.currentSlideId);
    if (!slide) return;
    try {
      const html2canvas = await ensureHtml2Canvas();
      const el = document.getElementById("ykt-slide-view");
      const canvas = await html2canvas(el, {
        useCORS: true,
        allowTaint: false
      });
      const a = document.createElement("a");
      a.download = `slide-${slide.id}.png`;
      a.href = canvas.toDataURL("image/png");
      a.click();
    } catch (e) {
      ui.toast(`截图失败: ${e.message}`);
    }
  }
  async function downloadPresentationPDF() {
    if (!repo.currentPresentationId) return ui.toast("请先在左侧选择一份课件");
    const pres = repo.presentations.get(repo.currentPresentationId);
    if (!pres || !Array.isArray(pres.slides) || pres.slides.length === 0) return ui.toast("未找到该课件的页面");
    // 是否导出全部页:沿用你面板的“切换全部/题目页”开关语义
        const showAll = !!ui.config.showAllSlides;
    const slides = pres.slides.filter(s => showAll || s.problem);
    if (slides.length === 0) return ui.toast("当前筛选下没有可导出的页面");
    try {
      // 1) 确保 jsPDF 就绪
      await ensureJsPDF();
      const {jsPDF: jsPDF} = window.jspdf || {};
      if (!jsPDF) throw new Error("jsPDF 未加载成功");
      // 2) A4 纸张(pt):595 x 842(竖版)
            const doc = new jsPDF({
        unit: "pt",
        format: "a4",
        orientation: "portrait"
      });
      const pageW = 595, pageH = 842;
      // 页边距(视觉更好看)
            const margin = 24;
      const maxW = pageW - margin * 2;
      const maxH = pageH - margin * 2;
      // 简单的图片加载器(拿到原始宽高以保持比例居中)
            const loadImage = src => new Promise((resolve, reject) => {
        const img = new Image;
        img.crossOrigin = "anonymous";
        img.onload = () => resolve(img);
        img.onerror = reject;
        img.src = src;
      });
      for (let i = 0; i < slides.length; i++) {
        const s = slides[i];
        const url = s.image || s.thumbnail;
        if (!url) {
          // 无图页可跳过,也可在此尝试 html2canvas 截图(复杂度更高,此处先跳过)
          if (i > 0) doc.addPage();
          continue;
        }
        // 3) 加载图片并按比例缩放到 A4
                const img = await loadImage(url);
        const iw = img.naturalWidth || img.width;
        const ih = img.naturalHeight || img.height;
        const r = Math.min(maxW / iw, maxH / ih);
        const w = Math.floor(iw * r);
        const h = Math.floor(ih * r);
        const x = Math.floor((pageW - w) / 2);
        const y = Math.floor((pageH - h) / 2);
        // 4) 首页直接画,后续页先 addPage
                if (i > 0) doc.addPage();
        // 通过 <img> 对象加图(jsPDF 自动推断类型;如需可改成 'PNG')
                doc.addImage(img, "PNG", x, y, w, h);
      }
      // 5) 文件名:保留课件标题或 id
            const name = (pres.title || `课件-${pres.id}`).replace(/[\\/:*?"<>|]/g, "_");
      doc.save(`${name}.pdf`);
    } catch (e) {
      ui.toast(`导出 PDF 失败:${e.message || e}`);
    }
  }
  var tpl$2 = '<div id="ykt-problem-list-panel" class="ykt-panel">\n  <div class="panel-header">\n    <h3>课堂习题列表</h3>\n    <span class="close-btn" id="ykt-problem-list-close"><i class="fas fa-times"></i></span>\n  </div>\n\n  <div class="panel-body">\n    <div id="ykt-problem-list" class="problem-list">\n      \x3c!-- 由 problem-list.js 动态填充:\n           .problem-row\n             .problem-title\n             .problem-meta\n             .problem-actions (查看 / AI解答 / 已作答) --\x3e\n    </div>\n  </div>\n</div>\n';
  // ==== [ADD] 工具方法 & 取题接口(兼容旧版多端点) ====
    function create(tag, cls) {
    const n = document.createElement(tag);
    if (cls) n.className = cls;
    return n;
  }
  const HEADERS = () => ({
    "Content-Type": "application/json",
    xtbz: "ykt",
    "X-Client": "h5",
    Authorization: "Bearer " + (typeof localStorage !== "undefined" ? localStorage.getItem("Authorization") || "" : "")
  });
  async function httpGet(url) {
    return new Promise((resolve, reject) => {
      try {
        const xhr = new XMLHttpRequest;
        xhr.open("GET", url, true);
        const h = HEADERS();
        for (const k in h) xhr.setRequestHeader(k, h[k]);
        xhr.onload = () => {
          try {
            resolve(JSON.parse(xhr.responseText));
          } catch {
            reject(new Error("解析响应失败"));
          }
        };
        xhr.onerror = () => reject(new Error("网络失败"));
        xhr.send();
      } catch (e) {
        reject(e);
      }
    });
  }
  // 兼容旧版:依次尝试多个端点,先成功先用
    async function fetchProblemDetail(problemId) {
    const candidates = [ `/api/v3/lesson/problem/detail?problemId=${problemId}`, `/api/v3/lesson/problem/get?problemId=${problemId}`, `/mooc-api/v1/lms/problem/detail?problem_id=${problemId}` ];
    for (const url of candidates) try {
      const resp = await httpGet(url);
      if (resp && typeof resp === "object" && (resp.code === 0 || resp.success === true)) return resp;
    } catch (_) {/* try next */}
    throw new Error("无法获取题目信息");
  }
  function pretty(obj) {
    try {
      return JSON.stringify(obj, null, 2);
    } catch {
      return String(obj);
    }
  }
  // ==== [ADD] 渲染行上的按钮(查看 / AI解答 / 刷新题目) ====
    function bindRowActions(row, e, prob) {
    const actionsBar = row.querySelector(".problem-actions");
    const btnGo = create("button");
    btnGo.textContent = "查看";
    btnGo.onclick = () => actions.navigateTo(e.presentationId, e.slide?.id || e.slideId);
    actionsBar.appendChild(btnGo);
    const btnAI = create("button");
    btnAI.textContent = "AI解答";
    btnAI.onclick = () => window.dispatchEvent(new CustomEvent("ykt:open-ai", {
      detail: {
        problemId: e.problemId
      }
    }));
    actionsBar.appendChild(btnAI);
    const btnRefresh = create("button");
    btnRefresh.textContent = "刷新题目";
    btnRefresh.onclick = async () => {
      row.classList.add("loading");
      try {
        const resp = await fetchProblemDetail(e.problemId);
        const detail = resp.data?.problem || resp.data || resp.result || {};
        const merged = Object.assign({}, prob || {}, detail, {
          problemId: e.problemId,
          problemType: e.problemType
        });
        repo.problems.set(e.problemId, merged);
        updateRow(row, e, merged);
        ui.toast("已刷新题目");
      } catch (err) {
        ui.toast("刷新失败:" + (err?.message || err));
      } finally {
        row.classList.remove("loading");
      }
    };
    actionsBar.appendChild(btnRefresh);
  }
  function updateRow(row, e, prob) {
    // 标题
    const title = row.querySelector(".problem-title");
    title.textContent = (prob?.body || e.body || prob?.title || `题目 ${e.problemId}`).slice(0, 120);
    // 元信息(含截止时间)
        const meta = row.querySelector(".problem-meta");
    const status = prob?.status || e.status || {};
    const answered = !!(prob?.result || status?.answered || status?.myAnswer);
    const endTime = Number(status?.endTime || prob?.endTime || e.endTime || 0) || void 0;
    meta.textContent = `PID: ${e.problemId} / 类型: ${e.problemType} / 状态: ${answered ? "已作答" : "未作答"} / 截止: ${endTime ? new Date(endTime).toLocaleString() : "未知"}`;
    // 容器
        let detail = row.querySelector(".problem-detail");
    if (!detail) {
      detail = create("div", "problem-detail");
      row.appendChild(detail);
    }
    detail.innerHTML = "";
    // ===== 显示“已作答答案” =====
        const answeredBox = create("div", "answered-box");
    const ansLabel = create("div", "label");
    ansLabel.textContent = "已作答答案";
    const ansPre = create("pre");
    ansPre.textContent = pretty(prob?.result || status?.myAnswer || {});
    answeredBox.appendChild(ansLabel);
    answeredBox.appendChild(ansPre);
    detail.appendChild(answeredBox);
    // ===== 手动答题(含补交) =====
        const editorBox = create("div", "editor-box");
    const editLabel = create("div", "label");
    editLabel.textContent = "手动答题(JSON)";
    const textarea = create("textarea");
    textarea.rows = 6;
    textarea.placeholder = '{"answers":[...]}';
    textarea.value = pretty(prob?.result || status?.myAnswer || prob?.suggested || {});
    editorBox.appendChild(editLabel);
    editorBox.appendChild(textarea);
    const submitBar = create("div", "submit-bar");
    // 保存(仅本地)
        const btnSaveLocal = create("button");
    btnSaveLocal.textContent = "保存(本地)";
    btnSaveLocal.onclick = () => {
      try {
        const parsed = JSON.parse(textarea.value || "{}");
        const merged = Object.assign({}, prob || {}, {
          result: parsed
        });
        repo.problems.set(e.problemId, merged);
        ui.toast("已保存到本地列表");
        updateRow(row, e, merged);
      } catch (err) {
        ui.toast("JSON 解析失败:" + (err?.message || err));
      }
    };
    submitBar.appendChild(btnSaveLocal);
    // 正常提交(过期则提示是否补交)
        const startTime = Number(status?.startTime || prob?.startTime || e.startTime || 0) || void 0;
    const btnSubmit = create("button");
    btnSubmit.textContent = "提交";
    btnSubmit.onclick = async () => {
      try {
        const result = JSON.parse(textarea.value || "{}");
        row.classList.add("loading");
        const {route: route} = await submitAnswer({
          problemId: e.problemId,
          problemType: e.problemType
        }, result, {
          startTime: startTime,
          endTime: endTime
        });
        ui.toast(route === "answer" ? "提交成功" : "补交成功");
        const merged = Object.assign({}, prob || {}, {
          result: result
        }, {
          status: {
            ...prob?.status || {},
            answered: true
          }
        });
        repo.problems.set(e.problemId, merged);
        updateRow(row, e, merged);
      } catch (err) {
        if (err?.name === "DeadlineError") ui.confirm("已过截止,是否执行补交?").then(async ok => {
          if (!ok) return;
          try {
            const result = JSON.parse(textarea.value || "{}");
            row.classList.add("loading");
            await submitAnswer({
              problemId: e.problemId,
              problemType: e.problemType
            }, result, {
              startTime: startTime,
              endTime: endTime,
              forceRetry: true
            });
            ui.toast("补交成功");
            const merged = Object.assign({}, prob || {}, {
              result: result
            }, {
              status: {
                ...prob?.status || {},
                answered: true
              }
            });
            repo.problems.set(e.problemId, merged);
            updateRow(row, e, merged);
          } catch (e2) {
            ui.toast("补交失败:" + (e2?.message || e2));
          } finally {
            row.classList.remove("loading");
          }
        }); else ui.toast("提交失败:" + (err?.message || err));
      } finally {
        row.classList.remove("loading");
      }
    };
    submitBar.appendChild(btnSubmit);
    // 强制补交
        const btnForceRetry = create("button");
    btnForceRetry.textContent = "强制补交";
    btnForceRetry.onclick = async () => {
      try {
        const result = JSON.parse(textarea.value || "{}");
        row.classList.add("loading");
        await submitAnswer({
          problemId: e.problemId,
          problemType: e.problemType
        }, result, {
          startTime: startTime,
          endTime: endTime,
          forceRetry: true
        });
        ui.toast("补交成功");
        const merged = Object.assign({}, prob || {}, {
          result: result
        }, {
          status: {
            ...prob?.status || {},
            answered: true
          }
        });
        repo.problems.set(e.problemId, merged);
        updateRow(row, e, merged);
      } catch (err) {
        ui.toast("补交失败:" + (err?.message || err));
      } finally {
        row.classList.remove("loading");
      }
    };
    submitBar.appendChild(btnForceRetry);
    editorBox.appendChild(submitBar);
    detail.appendChild(editorBox);
  }
  let mounted$2 = false;
  let root$2;
  function $$2(sel) {
    return document.querySelector(sel);
  }
  function mountProblemListPanel() {
    if (mounted$2) return root$2;
    const wrap = document.createElement("div");
    wrap.innerHTML = tpl$2;
    document.body.appendChild(wrap.firstElementChild);
    root$2 = document.getElementById("ykt-problem-list-panel");
    $$2("#ykt-problem-list-close")?.addEventListener("click", () => showProblemListPanel(false));
    window.addEventListener("ykt:open-problem-list", () => showProblemListPanel(true));
    mounted$2 = true;
    updateProblemList();
    return root$2;
  }
  function showProblemListPanel(visible = true) {
    mountProblemListPanel();
    root$2.classList.toggle("visible", !!visible);
    if (visible) updateProblemList();
  }
  function updateProblemList() {
    mountProblemListPanel();
    const container = $$2("#ykt-problem-list");
    container.innerHTML = "";
    (repo.encounteredProblems || []).forEach(e => {
      const prob = repo.problems.get(e.problemId) || {};
      const row = document.createElement("div");
      row.className = "problem-row";
      // 标题和元信息容器,内容由 updateRow 填充
            const title = document.createElement("div");
      title.className = "problem-title";
      row.appendChild(title);
      const meta = document.createElement("div");
      meta.className = "problem-meta";
      row.appendChild(meta);
      const actionsBar = document.createElement("div");
      actionsBar.className = "problem-actions";
      row.appendChild(actionsBar);
      // 绑定按钮(查看 / AI解答 / 刷新题目)
            bindRowActions(row, e, prob);
      // 渲染题目信息 + 已作答答案 + 手动提交/补交 UI
            updateRow(row, e, prob);
      container.appendChild(row);
    });
  }
  var tpl$1 = '<div id="ykt-active-problems-panel" class="ykt-active-wrapper">\n  <div id="ykt-active-problems" class="active-problems"></div>\n</div>\n';
  let mounted$1 = false;
  let root$1;
  function $$1(sel) {
    return document.querySelector(sel);
  }
  function mountActiveProblemsPanel() {
    if (mounted$1) return root$1;
    const wrap = document.createElement("div");
    wrap.innerHTML = tpl$1;
    document.body.appendChild(wrap.firstElementChild);
    root$1 = document.getElementById("ykt-active-problems-panel");
    mounted$1 = true;
    // 轻量刷新计时器
        setInterval(() => updateActiveProblems(), 1e3);
    return root$1;
  }
  function updateActiveProblems() {
    mountActiveProblemsPanel();
    const box = $$1("#ykt-active-problems");
    box.innerHTML = "";
    const now = Date.now();
    repo.problemStatus.forEach((status, pid) => {
      const p = repo.problems.get(pid);
      if (!p || p.result) return;
      const card = document.createElement("div");
      card.className = "active-problem-card";
      const title = document.createElement("div");
      title.className = "ap-title";
      title.textContent = (p.body || `题目 ${pid}`).slice(0, 80);
      card.appendChild(title);
      const remain = Math.max(0, Math.floor((status.endTime - now) / 1e3));
      const info = document.createElement("div");
      info.className = "ap-info";
      info.textContent = `剩余 ${remain}s`;
      card.appendChild(info);
      const bar = document.createElement("div");
      bar.className = "ap-actions";
      const go = document.createElement("button");
      go.textContent = "查看";
      go.onclick = () => actions.navigateTo(status.presentationId, status.slideId);
      bar.appendChild(go);
      const ai = document.createElement("button");
      ai.textContent = "AI 解答";
      ai.onclick = () => window.dispatchEvent(new CustomEvent("ykt:open-ai"));
      bar.appendChild(ai);
      card.appendChild(bar);
      box.appendChild(card);
    });
  }
  var tpl = '<div id="ykt-tutorial-panel" class="ykt-panel">\n  <div class="panel-header">\n    <h3>雨课堂助手使用教程</h3>\n    <h5>1.17.1<h5>\n    <span class="close-btn" id="ykt-tutorial-close"><i class="fas fa-times"></i></span>\n  </div>\n\n  <div class="panel-body">\n    <div class="tutorial-content">\n      <h4>功能介绍</h4>\n      <p>AI雨课堂助手是一个为雨课堂提供辅助功能的工具,可以帮助你更好地参与课堂互动。</p>\n      <p>项目仓库:<a href="https://github.com/ZaytsevZY/yuketang-helper-ai" target="_blank" rel="noopener">GitHub</a></p>\n      <p>脚本安装:<a href="https://greasyfork.org/zh-CN/scripts/531469-ai雨课堂助手" target="_blank" rel="noopener">GreasyFork</a></p>\n\n      <h4>工具栏按钮说明</h4>\n      <ul>\n        <li><i class="fas fa-bell"></i> <b>习题提醒</b>:切换是否在新习题出现时显示通知提示(蓝色=开启)。</li>\n        <li><i class="fas fa-file-powerpoint"></i> <b>课件浏览</b>:查看课件与题目页面。</li>\n        <li><i class="fas fa-robot"></i> <b>AI 解答</b>:向 AI 询问当前题目并显示建议答案。</li>\n        <li><i class="fas fa-magic-wand-sparkles"></i> <b>自动作答</b>:切换自动作答(蓝色=开启)。</li>\n        <li><i class="fas fa-cog"></i> <b>设置</b>:配置 API 密钥与自动作答参数。</li>\n        <li><i class="fas fa-question-circle"></i> <b>使用教程</b>:显示/隐藏当前教程页面。</li>\n      </ul>\n\n      <h4>自动作答</h4>\n      <ul>\n        <li>在设置中开启自动作答并配置延迟/随机延迟。</li>\n        <li>需要配置 <del>DeepSeek API</del> Kimi API 密钥。</li>\n        <li>答案来自 AI,结果仅供参考。</li>\n      </ul>\n\n      <h4>AI 解答</h4>\n      <ol>\n        <li>点击设置(<i class="fas fa-cog"></i>)填入 API Key。</li>\n        <li>点击 AI 解答(<i class="fas fa-robot"></i>)后会对“当前题目/最近遇到的题目”询问并解析。</li>\n      </ol>\n\n      <h4>注意事项</h4>\n      <p>1) 仅供学习参考,请独立思考;</p>\n      <p>2) 合理使用 API 额度;</p>\n      <p>3) 答案不保证 100% 正确;</p>\n      <p>4) 自动作答有一定风险,谨慎开启。</p>\n\n      <h4>联系方式</h4>\n      <ul>\n        <li>请在Github issue提出问题</li>\n      </ul>\n    </div>\n  </div>\n</div>\n';
  let mounted = false;
  let root;
  function $(sel) {
    return document.querySelector(sel);
  }
  function mountTutorialPanel() {
    if (mounted) return root;
    const host = document.createElement("div");
    host.innerHTML = tpl;
    document.body.appendChild(host.firstElementChild);
    root = document.getElementById("ykt-tutorial-panel");
    $("#ykt-tutorial-close")?.addEventListener("click", () => showTutorialPanel(false));
    mounted = true;
    return root;
  }
  function showTutorialPanel(visible = true) {
    mountTutorialPanel();
    root.classList.toggle("visible", !!visible);
  }
  function toggleTutorialPanel() {
    mountTutorialPanel();
    const vis = root.classList.contains("visible");
    showTutorialPanel(!vis);
    // 同步工具条按钮激活态(如果存在)
        const helpBtn = document.getElementById("ykt-btn-help");
    if (helpBtn) helpBtn.classList.toggle("active", !vis);
  }
  // src/ui/ui-api.js
    const _config = Object.assign({}, DEFAULT_CONFIG, storage.get("config", {}));
  _config.ai.kimiApiKey = storage.get("kimiApiKey", _config.ai.kimiApiKey);
  _config.TYPE_MAP = _config.TYPE_MAP || PROBLEM_TYPE_MAP;
  function saveConfig() {
    storage.set("config", _config);
  }
  // 面板层级管理
    let currentZIndex = 1e7;
  const ui = {
    get config() {
      return _config;
    },
    saveConfig: saveConfig,
    updatePresentationList: updatePresentationList,
    updateSlideView: updateSlideView,
    askAIForCurrent: askAIForCurrent,
    updateProblemList: updateProblemList,
    updateActiveProblems: updateActiveProblems,
    // 提升面板层级的辅助函数
    _bringToFront(panelElement) {
      if (panelElement && panelElement.classList.contains("visible")) {
        currentZIndex += 1;
        panelElement.style.zIndex = currentZIndex;
      }
    },
    // 修改后的面板显示函数,添加z-index管理
    showPresentationPanel(visible = true) {
      showPresentationPanel(visible);
      if (visible) {
        const panel = document.getElementById("ykt-presentation-panel");
        this._bringToFront(panel);
      }
    },
    showProblemListPanel(visible = true) {
      showProblemListPanel(visible);
      if (visible) {
        const panel = document.getElementById("ykt-problem-list-panel");
        this._bringToFront(panel);
      }
    },
    showAIPanel(visible = true) {
      showAIPanel(visible);
      if (visible) {
        const panel = document.getElementById("ykt-ai-answer-panel");
        this._bringToFront(panel);
      }
    },
    toggleSettingsPanel() {
      toggleSettingsPanel();
      // 检查面板是否变为可见状态
            const panel = document.getElementById("ykt-settings-panel");
      if (panel && panel.classList.contains("visible")) this._bringToFront(panel);
    },
    toggleTutorialPanel() {
      toggleTutorialPanel();
      // 检查面板是否变为可见状态
            const panel = document.getElementById("ykt-tutorial-panel");
      if (panel && panel.classList.contains("visible")) this._bringToFront(panel);
    },
    // 在 index.js 初始化时挂载一次
    _mountAll() {
      mountSettingsPanel();
      mountAIPanel();
      mountPresentationPanel();
      mountProblemListPanel();
      mountActiveProblemsPanel();
      mountTutorialPanel();
      window.addEventListener("ykt:open-ai", () => this.showAIPanel(true));
    },
    notifyProblem(problem, slide) {
      gm.notify({
        title: "雨课堂习题提示",
        text: this.getProblemDetail(problem),
        image: slide?.thumbnail || null,
        timeout: 5e3
      });
    },
    getProblemDetail(problem) {
      if (!problem) return "题目未找到";
      const lines = [ problem.body || "" ];
      if (Array.isArray(problem.options)) lines.push(...problem.options.map(({key: key, value: value}) => `${key}. ${value}`));
      return lines.join("\n");
    },
    toast: toast,
    nativeNotify: gm.notify,
    // Buttons 状态
    updateAutoAnswerBtn() {
      const el = document.getElementById("ykt-btn-auto-answer");
      if (!el) return;
      if (_config.autoAnswer) el.classList.add("active"); else el.classList.remove("active");
    }
  };
  // src/state/actions.js
  // 内部自动答题处理函数
    async function handleAutoAnswerInternal(problem) {
    const status = repo.problemStatus.get(problem.problemId);
    if (!status || status.answering || problem.result) return;
    if (Date.now() >= status.endTime) return;
    try {
      let aiAnswer, parsed;
      // 优先使用文本模式,如果没有题干则使用Vision模式
            if (problem.body && problem.body.trim()) {
        const q = formatProblemForAI(problem, PROBLEM_TYPE_MAP);
        aiAnswer = await queryKimi(q, ui.config.ai);
        parsed = parseAIAnswer(problem, aiAnswer);
      }
      // 如果文本模式失败或没有题干,尝试Vision模式
            if (!parsed) {
        const {captureProblemForVision: captureProblemForVision} = await Promise.resolve().then(function() {
          return screenshoot;
        });
        const {queryKimiVision: queryKimiVision} = await Promise.resolve().then(function() {
          return kimi;
        });
        const imageBase64 = await captureProblemForVision();
        if (imageBase64) {
          const textPrompt = problem.body ? `请结合题目信息分析图片:\n${formatProblemForAI(problem, PROBLEM_TYPE_MAP)}` : "请分析图片中的题目并给出答案";
          aiAnswer = await queryKimiVision(imageBase64, textPrompt, ui.config.ai);
          parsed = parseAIAnswer(problem, aiAnswer);
        }
      }
      if (!parsed) return ui.toast("无法解析AI答案,跳过自动作答", 2e3);
      await submitAnswer(problem, parsed);
      actions.onAnswerProblem(problem.problemId, parsed);
      ui.toast(`自动作答完成: ${String(problem.body || "").slice(0, 30)}...`, 3e3);
      showAutoAnswerPopup(problem, typeof aiAnswer === "string" ? aiAnswer : JSON.stringify(aiAnswer, null, 2));
    } catch (e) {
      console.error("[AutoAnswer] failed", e);
      ui.toast(`自动作答失败: ${e.message}`, 3e3);
    }
  }
  const actions = {
    onFetchTimeline(timeline) {
      for (const piece of timeline) if (piece.type === "problem") this.onUnlockProblem(piece);
    },
    onPresentationLoaded(id, data) {
      repo.setPresentation(id, data);
      const pres = repo.presentations.get(id);
      for (const slide of pres.slides) {
        repo.upsertSlide(slide);
        if (slide.problem) {
          repo.upsertProblem(slide.problem);
          repo.pushEncounteredProblem(slide.problem, slide, id);
        }
      }
      ui.updatePresentationList();
    },
    onUnlockProblem(data) {
      const problem = repo.problems.get(data.prob);
      const slide = repo.slides.get(data.sid);
      if (!problem || !slide) return;
      const status = {
        presentationId: data.pres,
        slideId: data.sid,
        startTime: data.dt,
        endTime: data.dt + 1e3 * data.limit,
        done: !!problem.result,
        autoAnswerTime: null,
        answering: false
      };
      repo.problemStatus.set(data.prob, status);
      if (Date.now() > status.endTime || problem.result) return;
      // toast + 通知
            if (ui.config.notifyProblems) ui.notifyProblem(problem, slide);
      // 自动作答
            if (ui.config.autoAnswer) {
        const delay = ui.config.autoAnswerDelay + randInt(0, ui.config.autoAnswerRandomDelay);
        status.autoAnswerTime = Date.now() + delay;
        ui.toast(`将在 ${Math.floor(delay / 1e3)} 秒后自动作答本题`, 3e3);
      }
      ui.updateActiveProblems();
    },
    onLessonFinished() {
      ui.nativeNotify({
        title: "下课提示",
        text: "当前课程已结束",
        timeout: 5e3
      });
    },
    onAnswerProblem(problemId, result) {
      const p = repo.problems.get(problemId);
      if (p) {
        p.result = result;
        const i = repo.encounteredProblems.findIndex(e => e.problemId === problemId);
        if (i !== -1) repo.encounteredProblems[i].result = result;
        ui.updateProblemList();
      }
    },
    async handleAutoAnswer(problem) {
      return handleAutoAnswerInternal(problem);
    },
    // 定时器驱动(由 index.js 安装)
    tickAutoAnswer() {
      const now = Date.now();
      for (const [pid, status] of repo.problemStatus) if (status.autoAnswerTime !== null && now >= status.autoAnswerTime) {
        const p = repo.problems.get(pid);
        if (p) {
          status.autoAnswerTime = null;
          this.handleAutoAnswer(p);
        }
      }
    },
    async submit(problem, content) {
      const result = this.parseManual(problem.problemType, content);
      await submitAnswer(problem, result);
      this.onAnswerProblem(problem.problemId, result);
    },
    parseManual(problemType, content) {
      switch (problemType) {
       case 1:
       case 2:
       case 3:
        return content.split("").sort();

       case 4:
        return content.split("\n").filter(Boolean);

       case 5:
        return {
          content: content,
          pics: []
        };

       default:
        return null;
      }
    },
    navigateTo(presId, slideId) {
      repo.currentPresentationId = presId;
      repo.currentSlideId = slideId;
      ui.updateSlideView();
      ui.showPresentationPanel(true);
    },
    //1.16.4: 进入课堂:设置 lessonId +(可选)写入 Tab 信息 + 载入本课已存课件
    launchLessonHelper() {
      // 从 URL 提取 lessonId(/lesson/fullscreen/v3/<lessonId>/...)
      const path = window.location.pathname;
      const m = path.match(/\/lesson\/fullscreen\/v3\/([^/]+)/);
      repo.currentLessonId = m ? m[1] : null;
      if (repo.currentLessonId) console.log(`[雨课堂助手] 检测到课堂页面 lessonId: ${repo.currentLessonId}`);
      // GM_* Tab 状态(存在才用,向后兼容)
            if (typeof window.GM_getTab === "function" && typeof window.GM_saveTab === "function" && repo.currentLessonId) window.GM_getTab(tab => {
        tab.type = "lesson";
        tab.lessonId = repo.currentLessonId;
        window.GM_saveTab(tab);
      });
      // 载入"本课"的历史课件
            repo.loadStoredPresentations();
    }
  };
  // src/net/ws-interceptor.js
    function installWSInterceptor() {
    // 环境识别(标准/荷塘/未知),主要用于日志和后续按需适配
    function detectEnvironmentAndAdaptAPI() {
      const hostname = location.hostname;
      let envType = "unknown";
      if (hostname === "www.yuketang.cn") {
        envType = "standard";
        console.log("[雨课堂助手] 检测到标准雨课堂环境");
      } else if (hostname === "pro.yuketang.cn") {
        envType = "pro";
        console.log("[雨课堂助手] 检测到荷塘雨课堂环境");
      } else console.log("[雨课堂助手] 未知环境:", hostname);
      return envType;
    }
    class MyWebSocket extends WebSocket {
      static handlers=[];
      static addHandler(h) {
        this.handlers.push(h);
      }
      constructor(url, protocols) {
        super(url, protocols);
        const parsed = new URL(url, location.href);
        for (const h of this.constructor.handlers) h(this, parsed);
      }
      intercept(cb) {
        const raw = this.send;
        this.send = data => {
          try {
            cb(JSON.parse(data));
          } catch {}
          return raw.call(this, data);
        };
      }
      listen(cb) {
        this.addEventListener("message", e => {
          try {
            cb(JSON.parse(e.data));
          } catch {}
        });
      }
    }
    // MyWebSocket.addHandler((ws, url) => {
    //   if (url.pathname === '/wsapp/') {
    //     ws.listen((msg) => {
    //       switch (msg.op) {
    //         case 'fetchtimeline': actions.onFetchTimeline(msg.timeline); break;
    //         case 'unlockproblem': actions.onUnlockProblem(msg.problem); break;
    //         case 'lessonfinished': actions.onLessonFinished(); break;
    //       }
    //     });
    //   }
    // });
        MyWebSocket.addHandler((ws, url) => {
      const envType = detectEnvironmentAndAdaptAPI();
      console.log("[雨课堂助手] 拦截WebSocket通信 - 环境:", envType);
      console.log("[雨课堂助手] WebSocket连接尝试:", url.href);
      // 更宽松的路径匹配
            const wsPath = url.pathname || "";
      const isRainClassroomWS = wsPath === "/wsapp/" || wsPath.includes("/ws") || wsPath.includes("/websocket") || url.href.includes("websocket");
      if (!isRainClassroomWS) {
        console.log("[雨课堂助手] ❌ 非雨课堂WebSocket:", wsPath);
        return;
      }
      console.log("[雨课堂助手] ✅ 检测到雨课堂WebSocket连接:", wsPath);
      // 发送侧拦截(可用于调试)
            ws.intercept(message => {
        console.log("[雨课堂助手] WebSocket发送:", message);
      });
      // 接收侧统一分发
            ws.listen(message => {
        try {
          console.log("[雨课堂助手] WebSocket接收:", message);
          switch (message.op) {
           case "fetchtimeline":
            console.log("[雨课堂助手] 收到时间线:", message.timeline);
            actions.onFetchTimeline(message.timeline);
            break;

           case "unlockproblem":
            console.log("[雨课堂助手] 收到解锁问题:", message.problem);
            actions.onUnlockProblem(message.problem);
            break;

           case "lessonfinished":
            console.log("[雨课堂助手] 课程结束");
            actions.onLessonFinished();
            break;

           default:
            console.log("[雨课堂助手] 未知WebSocket操作:", message.op, message);
          }
        } catch (e) {
          console.debug("[雨课堂助手] 解析WebSocket消息失败", e, message);
        }
      });
    });
    gm.uw.WebSocket = MyWebSocket;
  }
  // src/net/xhr-interceptor.js
    function installXHRInterceptor() {
    class MyXHR extends XMLHttpRequest {
      static handlers=[];
      static addHandler(h) {
        this.handlers.push(h);
      }
      open(method, url, async) {
        const parsed = new URL(url, location.href);
        for (const h of this.constructor.handlers) h(this, method, parsed);
        return super.open(method, url, async ?? true);
      }
      intercept(cb) {
        let payload;
        const rawSend = this.send;
        this.send = body => {
          payload = body;
          return rawSend.call(this, body);
        };
        this.addEventListener("load", () => {
          try {
            cb(JSON.parse(this.responseText), payload);
          } catch {}
        });
      }
    }
    function detectEnvironmentAndAdaptAPI() {
      const hostname = location.hostname;
      if (hostname === "www.yuketang.cn") {
        console.log("[雨课堂助手] 检测到标准雨课堂环境");
        return "standard";
      }
      if (hostname === "pro.yuketang.cn") {
        console.log("[雨课堂助手] 检测到荷塘雨课堂环境");
        return "pro";
      }
      console.log("[雨课堂助手] 未知环境:", hostname);
      return "unknown";
    }
    MyXHR.addHandler((xhr, method, url) => {
      detectEnvironmentAndAdaptAPI();
      const pathname = url.pathname || "";
      console.log("[雨课堂助手] XHR请求:", method, pathname, url.search);
      // 课件:精确路径或包含关键字
            if (pathname === "/api/v3/lesson/presentation/fetch" || pathname.includes("presentation") && pathname.includes("fetch")) {
        console.log("[雨课堂助手] ✅ 拦截课件请求");
        xhr.intercept(resp => {
          const id = url.searchParams.get("presentation_id");
          console.log("[雨课堂助手] 课件响应:", resp);
          if (resp && (resp.code === 0 || resp.success)) actions.onPresentationLoaded(id, resp.data || resp.result);
        });
        return;
      }
      // 答题
            if (pathname === "/api/v3/lesson/problem/answer" || pathname.includes("problem") && pathname.includes("answer")) {
        console.log("[雨课堂助手] ✅ 拦截答题请求");
        xhr.intercept((resp, payload) => {
          try {
            const {problemId: problemId, result: result} = JSON.parse(payload || "{}");
            if (resp && (resp.code === 0 || resp.success)) actions.onAnswerProblem(problemId, result);
          } catch (e) {
            console.error("[雨课堂助手] 解析答题响应失败:", e);
          }
        });
        return;
      }
      if (url.pathname === "/api/v3/lesson/problem/retry") {
        xhr.intercept((resp, payload) => {
          try {
            // retry 请求体是 { problems: [{ problemId, result, ...}] }
            const body = JSON.parse(payload || "{}");
            const first = Array.isArray(body?.problems) ? body.problems[0] : null;
            if (resp?.code === 0 && first?.problemId) actions.onAnswerProblem(first.problemId, first.result);
          } catch {}
        });
        return;
      }
      if (pathname.includes("/api/")) console.log("[雨课堂助手] 其他API:", method, pathname);
    });
    gm.uw.XMLHttpRequest = MyXHR;
  }
  var css = '/* ===== 通用 & 修复 ===== */\n#watermark_layer { display: none !important; visibility: hidden !important; }\n.hidden { display: none !important; }\n\n:root{\n  --ykt-z: 10000000;\n  --ykt-border: #ddd;\n  --ykt-border-strong: #ccc;\n  --ykt-bg: #fff;\n  --ykt-fg: #222;\n  --ykt-muted: #607190;\n  --ykt-accent: #1d63df;\n  --ykt-hover: #1e3050;\n  --ykt-shadow: 0 10px 30px rgba(0,0,0,.18);\n}\n\n/* ===== 工具栏 ===== */\n#ykt-helper-toolbar{\n  position: fixed; z-index: calc(var(--ykt-z) + 1);\n  left: 15px; bottom: 15px;\n  /* 移除固定宽度,让内容自适应 */\n  height: 36px; padding: 5px;\n  display: flex; gap: 6px; align-items: center;\n  background: var(--ykt-bg);\n  border: 1px solid var(--ykt-border-strong);\n  border-radius: 4px;\n  box-shadow: 0 1px 4px 3px rgba(0,0,0,.1);\n}\n\n#ykt-helper-toolbar .btn{\n  display: inline-block; padding: 4px; cursor: pointer;\n  color: var(--ykt-muted); line-height: 1;\n}\n#ykt-helper-toolbar .btn:hover{ color: var(--ykt-hover); }\n#ykt-helper-toolbar .btn.active{ color: var(--ykt-accent); }\n\n/* ===== 面板通用样式 ===== */\n.ykt-panel{\n  position: fixed; right: 20px; bottom: 60px;\n  width: 560px; max-height: 72vh; overflow: auto;\n  background: var(--ykt-bg); color: var(--ykt-fg);\n  border: 1px solid var(--ykt-border-strong); border-radius: 8px;\n  box-shadow: var(--ykt-shadow);\n  display: none; \n  /* 提高z-index,确保后打开的面板在最上层 */\n  z-index: var(--ykt-z);\n}\n.ykt-panel.visible{ \n  display: block; \n  /* 动态提升z-index */\n  z-index: calc(var(--ykt-z) + 10);\n}\n\n.panel-header{\n  display: flex; align-items: center; justify-content: space-between;\n  gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--ykt-border);\n}\n.panel-header h3{ margin: 0; font-size: 16px; font-weight: 600; }\n.panel-body{ padding: 10px 12px; }\n.close-btn{ cursor: pointer; color: var(--ykt-muted); }\n.close-btn:hover{ color: var(--ykt-hover); }\n\n/* ===== 设置面板 (#ykt-settings-panel) ===== */\n#ykt-settings-panel .settings-content{ display: flex; flex-direction: column; gap: 14px; }\n#ykt-settings-panel .setting-group{ border: 1px dashed var(--ykt-border); border-radius: 6px; padding: 10px; }\n#ykt-settings-panel .setting-group h4{ margin: 0 0 8px 0; font-size: 14px; }\n#ykt-settings-panel .setting-item{ display: flex; align-items: center; gap: 8px; margin: 8px 0; flex-wrap: wrap; }\n#ykt-settings-panel label{ font-size: 13px; }\n#ykt-settings-panel input[type="text"],\n#ykt-settings-panel input[type="number"]{\n  height: 30px; border: 1px solid var(--ykt-border-strong);\n  border-radius: 4px; padding: 0 8px; min-width: 220px;\n}\n#ykt-settings-panel small{ color: #666; }\n#ykt-settings-panel .setting-actions{ display: flex; gap: 8px; margin-top: 6px; }\n#ykt-settings-panel button{\n  height: 30px; padding: 0 12px; border-radius: 6px;\n  border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer;\n}\n#ykt-settings-panel button:hover{ background: #eef3ff; border-color: var(--ykt-accent); }\n\n/* 自定义复选框(与手写脚本一致的视觉语义) */\n#ykt-settings-panel .checkbox-label{ position: relative; padding-left: 26px; cursor: pointer; user-select: none; }\n#ykt-settings-panel .checkbox-label input{ position: absolute; opacity: 0; cursor: pointer; height: 0; width: 0; }\n#ykt-settings-panel .checkbox-label .checkmark{\n  position: absolute; left: 0; top: 50%; transform: translateY(-50%);\n  height: 16px; width: 16px; border:1px solid var(--ykt-border-strong); border-radius: 3px; background: #fff;\n}\n#ykt-settings-panel .checkbox-label input:checked ~ .checkmark{\n  background: var(--ykt-accent); border-color: var(--ykt-accent);\n}\n#ykt-settings-panel .checkbox-label .checkmark:after{\n  content: ""; position: absolute; display: none;\n  left: 5px; top: 1px; width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);\n}\n#ykt-settings-panel .checkbox-label input:checked ~ .checkmark:after{ display: block; }\n\n/* ===== AI 解答面板 (#ykt-ai-answer-panel) ===== */\n#ykt-ai-answer-panel .ai-question{\n  white-space: pre-wrap; background: #fafafa; border: 1px solid var(--ykt-border);\n  padding: 8px; border-radius: 6px; margin-bottom: 8px; max-height: 160px; overflow: auto;\n}\n#ykt-ai-answer-panel .ai-loading{ color: var(--ykt-accent); margin-bottom: 6px; }\n#ykt-ai-answer-panel .ai-error{ color: #b00020; margin-bottom: 6px; }\n#ykt-ai-answer-panel .ai-answer{ white-space: pre-wrap; margin-top: 4px; }\n#ykt-ai-answer-panel .ai-actions{ margin-top: 10px; }\n#ykt-ai-answer-panel .ai-actions button{\n  height: 30px; padding: 0 12px; border-radius: 6px;\n  border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer;\n}\n#ykt-ai-answer-panel .ai-actions button:hover{ background: #eef3ff; border-color: var(--ykt-accent); }\n\n/* ===== 课件浏览面板 (#ykt-presentation-panel) ===== */\n#ykt-presentation-panel{ width: 900px; }\n#ykt-presentation-panel .panel-controls{ display: flex; align-items: center; gap: 8px; }\n#ykt-presentation-panel .panel-body{\n  display: grid; grid-template-columns: 300px 1fr; gap: 10px;\n}\n#ykt-presentation-panel .presentation-title{\n  font-weight: 600; padding: 6px 0; border-bottom: 1px solid var(--ykt-border);\n}\n#ykt-presentation-panel .slide-thumb-list{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }\n#ykt-presentation-panel .slide-thumb{\n  border: 1px solid var(--ykt-border); border-radius: 6px; background: #fafafa;\n  min-height: 60px; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 4px; text-align: center;\n}\n#ykt-presentation-panel .slide-thumb:hover{ border-color: var(--ykt-accent); background: #eef3ff; }\n#ykt-presentation-panel .slide-thumb img{ max-width: 100%; max-height: 120px; object-fit: contain; display: block; }\n\n#ykt-presentation-panel .slide-view{\n  position: relative; border: 1px solid var(--ykt-border); border-radius: 8px; min-height: 360px; background: #fff; overflow: hidden;\n}\n#ykt-presentation-panel .slide-cover{ display: flex; align-items: center; justify-content: center; min-height: 360px; }\n#ykt-presentation-panel .slide-cover img{ max-width: 100%; max-height: 100%; object-fit: contain; display: block; }\n\n#ykt-presentation-panel .problem-box{\n  position: absolute; left: 12px; right: 12px; bottom: 12px;\n  background: rgba(255,255,255,.96); border: 1px solid var(--ykt-border);\n  border-radius: 8px; padding: 10px; box-shadow: 0 6px 18px rgba(0,0,0,.12);\n}\n#ykt-presentation-panel .problem-head{ font-weight: 600; margin-bottom: 6px; }\n#ykt-presentation-panel .problem-options{ display: grid; grid-template-columns: 1fr; gap: 4px; }\n#ykt-presentation-panel .problem-option{ padding: 6px 8px; border: 1px solid var(--ykt-border); border-radius: 6px; background: #fafafa; }\n\n/* ===== 题目列表面板 (#ykt-problem-list-panel) ===== */\n#ykt-problem-list{ display: flex; flex-direction: column; gap: 10px; }\n#ykt-problem-list .problem-row{\n  border: 1px solid var(--ykt-border); border-radius: 8px; padding: 8px; background: #fafafa;\n}\n#ykt-problem-list .problem-title{ font-weight: 600; margin-bottom: 4px; }\n#ykt-problem-list .problem-meta{ color: #666; font-size: 12px; margin-bottom: 6px; }\n#ykt-problem-list .problem-actions{ display: flex; gap: 8px; align-items: center; }\n#ykt-problem-list .problem-actions button{\n  height: 28px; padding: 0 10px; border-radius: 6px; border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer;\n}\n#ykt-problem-list .problem-actions button:hover{ background: #eef3ff; border-color: var(--ykt-accent); }\n#ykt-problem-list .problem-done{ color: #0a7a2f; font-weight: 600; }\n\n/* ===== 活动题目列表(右下角小卡片) ===== */\n#ykt-active-problems-panel.ykt-active-wrapper{\n  position: fixed; right: 20px; bottom: 60px; z-index: var(--ykt-z);\n}\n#ykt-active-problems{ display: flex; flex-direction: column; gap: 8px; max-height: 60vh; overflow: auto; }\n#ykt-active-problems .active-problem-card{\n  width: 320px; background: #fff; border: 1px solid var(--ykt-border);\n  border-radius: 8px; box-shadow: var(--ykt-shadow); padding: 10px;\n}\n#ykt-active-problems .ap-title{ font-weight: 600; margin-bottom: 4px; }\n#ykt-active-problems .ap-info{ color: #666; font-size: 12px; margin-bottom: 8px; }\n#ykt-active-problems .ap-actions{ display: flex; gap: 8px; }\n#ykt-active-problems .ap-actions button{\n  height: 28px; padding: 0 10px; border-radius: 6px; border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer;\n}\n#ykt-active-problems .ap-actions button:hover{ background: #eef3ff; border-color: var(--ykt-accent); }\n\n/* ===== 教程面板 (#ykt-tutorial-panel) ===== */\n#ykt-tutorial-panel .tutorial-content h4{ margin: 8px 0 6px; }\n#ykt-tutorial-panel .tutorial-content p,\n#ykt-tutorial-panel .tutorial-content li{ line-height: 1.5; }\n#ykt-tutorial-panel .tutorial-content a{ color: var(--ykt-accent); text-decoration: none; }\n#ykt-tutorial-panel .tutorial-content a:hover{ text-decoration: underline; }\n\n/* ===== 小屏适配 ===== */\n@media (max-width: 1200px){\n  #ykt-presentation-panel{ width: 760px; }\n  #ykt-presentation-panel .panel-body{ grid-template-columns: 260px 1fr; }\n}\n@media (max-width: 900px){\n  .ykt-panel{ right: 12px; left: 12px; width: auto; }\n  #ykt-presentation-panel{ width: auto; }\n  #ykt-presentation-panel .panel-body{ grid-template-columns: 1fr; }\n}\n\n/* ===== 自动作答成功弹窗 ===== */\n.auto-answer-popup{\n  position: fixed; inset: 0; z-index: calc(var(--ykt-z) + 2);\n  background: rgba(0,0,0,.2);\n  display: flex; align-items: flex-end; justify-content: flex-end;\n  opacity: 0; transition: opacity .18s ease;\n}\n.auto-answer-popup.visible{ opacity: 1; }\n\n.auto-answer-popup .popup-content{\n  width: min(560px, 96vw);\n  background: #fff; border: 1px solid var(--ykt-border-strong);\n  border-radius: 10px; box-shadow: var(--ykt-shadow);\n  margin: 16px; overflow: hidden;\n}\n\n.auto-answer-popup .popup-header{\n  display: flex; align-items: center; justify-content: space-between;\n  gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--ykt-border);\n}\n.auto-answer-popup .popup-header h4{ margin: 0; font-size: 16px; }\n.auto-answer-popup .close-btn{ cursor: pointer; color: var(--ykt-muted); }\n.auto-answer-popup .close-btn:hover{ color: var(--ykt-hover); }\n\n.auto-answer-popup .popup-body{ padding: 10px 12px; display: flex; flex-direction: column; gap: 10px; }\n.auto-answer-popup .popup-row{ display: grid; grid-template-columns: 56px 1fr; gap: 8px; align-items: start; }\n.auto-answer-popup .label{ color: #666; font-size: 12px; line-height: 1.8; }\n.auto-answer-popup .content{ white-space: normal; word-break: break-word; }\n\n/* ===== 1.16.6: 课件浏览面板:固定右侧详细视图,左侧独立滚动 ===== */\n#ykt-presentation-panel {\n  --ykt-panel-max-h: 72vh;           /* 与 .ykt-panel 的最大高度保持一致 */\n}\n\n/* 两列布局:左列表 + 右详细视图 */\n#ykt-presentation-panel .panel-body{\n  display: grid;\n  grid-template-columns: 300px 1fr;  /* 左列宽度可按需调整 */\n  gap: 12px;\n  overflow: hidden;                  /* 避免内部再出现双滚动条 */\n  align-items: start;\n}\n\n/* 左侧:只让左列滚动,限制在面板可视高度内 */\n#ykt-presentation-panel .panel-left{\n  max-height: var(--ykt-panel-max-h);\n  overflow: auto;\n  min-width: 0;                      /* 防止子元素撑破 */\n}\n\n/* 右侧:粘性定位为“固定”,始终在面板可视区内 */\n#ykt-presentation-panel .panel-right{\n  position: sticky;\n  top: 0;                            /* 相对可滚动祖先(面板)吸顶 */\n  align-self: start;\n}\n\n/* 右侧详细视图自身也限制高度并允许内部滚动 */\n#ykt-presentation-panel .slide-view{\n  max-height: var(--ykt-panel-max-h);\n  overflow: auto;\n  border: 1px solid var(--ykt-border);\n  border-radius: 8px;\n  background: #fff;\n}\n\n/* 小屏自适配:堆叠布局时取消 sticky,避免遮挡 */\n@media (max-width: 900px){\n  #ykt-presentation-panel .panel-body{\n    grid-template-columns: 1fr;\n  }\n  #ykt-presentation-panel .panel-right{\n    position: static;\n  }\n}\n';
  // src/ui/styles.js
    function injectStyles() {
    gm.addStyle(css);
  }
  // src/ui/toolbar.js
    function installToolbar() {
    // 仅创建容器与按钮;具体面板之后用 HTML/Vue 接入
    const bar = document.createElement("div");
    bar.id = "ykt-helper-toolbar";
    bar.innerHTML = `\n    <span id="ykt-btn-bell" class="btn" title="习题提醒"><i class="fas fa-bell"></i></span>\n    <span id="ykt-btn-pres" class="btn" title="课件浏览"><i class="fas fa-file-powerpoint"></i></span>\n    <span id="ykt-btn-ai" class="btn" title="AI解答"><i class="fas fa-robot"></i></span>\n    <span id="ykt-btn-auto-answer" class="btn" title="自动作答"><i class="fas fa-magic-wand-sparkles"></i></span>\n    <span id="ykt-btn-settings" class="btn" title="设置"><i class="fas fa-cog"></i></span>\n    <span id="ykt-btn-help" class="btn" title="使用教程"><i class="fas fa-question-circle"></i></span>\n  `;
    document.body.appendChild(bar);
    // 初始激活态
        if (ui.config.notifyProblems) bar.querySelector("#ykt-btn-bell")?.classList.add("active");
    ui.updateAutoAnswerBtn();
    // 事件绑定
        bar.querySelector("#ykt-btn-bell")?.addEventListener("click", () => {
      ui.config.notifyProblems = !ui.config.notifyProblems;
      ui.saveConfig();
      ui.toast(`习题提醒:${ui.config.notifyProblems ? "开" : "关"}`);
      bar.querySelector("#ykt-btn-bell")?.classList.toggle("active", ui.config.notifyProblems);
    });
    // 修改课件浏览按钮 - 切换显示/隐藏
        bar.querySelector("#ykt-btn-pres")?.addEventListener("click", () => {
      const btn = bar.querySelector("#ykt-btn-pres");
      const isActive = btn.classList.contains("active");
      ui.showPresentationPanel?.(!isActive);
      btn.classList.toggle("active", !isActive);
    });
    // 修改AI按钮 - 切换显示/隐藏
        bar.querySelector("#ykt-btn-ai")?.addEventListener("click", () => {
      const btn = bar.querySelector("#ykt-btn-ai");
      const isActive = btn.classList.contains("active");
      ui.showAIPanel?.(!isActive);
      btn.classList.toggle("active", !isActive);
    });
    bar.querySelector("#ykt-btn-auto-answer")?.addEventListener("click", () => {
      ui.config.autoAnswer = !ui.config.autoAnswer;
      ui.saveConfig();
      ui.toast(`自动作答:${ui.config.autoAnswer ? "开" : "关"}`);
      ui.updateAutoAnswerBtn();
    });
    bar.querySelector("#ykt-btn-settings")?.addEventListener("click", () => {
      ui.toggleSettingsPanel?.();
    });
    bar.querySelector("#ykt-btn-help")?.addEventListener("click", () => {
      ui.toggleTutorialPanel?.();
    });
  }
  // src/index.js
  // 可选:统一放到 core/env.js 的 ensureFontAwesome;这里保留现有注入方式也可以
    (function loadFA() {
    const link = document.createElement("link");
    link.rel = "stylesheet";
    link.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css";
    document.head.appendChild(link);
  })();
  (function main() {
    // 1) 样式/图标
    injectStyles();
    // 2) 先挂 UI(面板、事件桥接)
        ui._mountAll?.();
 // ✅ 现在 ui 已导入,确保执行到位
    // 3) 再装网络拦截
        installWSInterceptor();
    installXHRInterceptor();
    // 4) 装工具条(按钮会用到 ui.config 状态)
        installToolbar();
    // 5) 启动自动作答轮询(替代原来的 tickAutoAnswer 占位)
        actions.startAutoAnswerLoop();
    // 6)1.16.4 更新课件加载
        actions.launchLessonHelper();
  })();
})();