Greasy Fork

Greasy Fork is available in English.

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         AI雨课堂助手(模块化构建版)
// @namespace    https://github.com/ZaytsevZY/yuketang-helper-auto
// @version      1.18.6
// @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,
    currentSelectedUrl: 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">\r\n  <div class="panel-header">\r\n    <h3>AI雨课堂助手设置</h3>\r\n    <span class="close-btn" id="ykt-settings-close"><i class="fas fa-times"></i></span>\r\n  </div>\r\n\r\n  <div class="panel-body">\r\n    <div class="settings-content">\r\n      <div class="setting-group">\r\n        <h4>AI配置</h4>\r\n          \x3c!-- 将DeepSeek相关配置替换为Kimi --\x3e\r\n          <div class="setting-item">\r\n              <label for="kimi-api-key">Kimi API Key:</label>\r\n              <input type="password" id="kimi-api-key" placeholder="输入您的 Kimi API Key">\r\n              <small>从 <a href="https://platform.moonshot.cn/" target="_blank">Kimi开放平台</a> 获取</small>\r\n          </div>\r\n      </div>\r\n\r\n      <div class="setting-group">\r\n        <h4>自动作答设置</h4>\r\n        <div class="setting-item">\r\n          <label class="checkbox-label">\r\n            <input type="checkbox" id="ykt-input-auto-answer">\r\n            <span class="checkmark"></span>\r\n            启用自动作答\r\n          </label>\r\n        </div>\r\n        <div class="setting-item">\r\n          <label class="checkbox-label">\r\n            <input type="checkbox" id="ykt-input-ai-auto-analyze">\r\n            <span class="checkmark"></span>\r\n            打开 AI 页面时自动分析\r\n          </label>\r\n          <small>开启后,进入“AI 解答”面板即自动向 AI 询问当前题目</small>\r\n        </div>\r\n        <div class="setting-item">\r\n          <label for="ykt-input-answer-delay">作答延迟时间 (秒):</label>\r\n          <input type="number" id="ykt-input-answer-delay" min="1" max="60">\r\n          <small>题目出现后等待多长时间开始作答</small>\r\n        </div>\r\n        <div class="setting-item">\r\n          <label for="ykt-input-random-delay">随机延迟范围 (秒):</label>\r\n          <input type="number" id="ykt-input-random-delay" min="0" max="30">\r\n          <small>在基础延迟基础上随机增加的时间范围</small>\r\n        </div><div class="setting-item">\r\n          <label class="checkbox-label">\r\n            <input type="checkbox" id="ykt-ai-pick-main-first">\r\n            <span class="checkmark"></span>\r\n            主界面优先(未勾选则课件浏览优先)\r\n          </label>\r\n          <small>仅在普通打开 AI 面板(ykt:open-ai)时生效;从“提问当前PPT”跳转保持最高优先。</small>\r\n        </div>\r\n      </div>       \r\n      <div class="setting-actions">\r\n        <button id="ykt-btn-settings-save">保存设置</button>\r\n        <button id="ykt-btn-settings-reset">重置为默认</button>\r\n      </div>\r\n    </div>\r\n  </div>\r\n</div>\r\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");
    const $priorityRadios = root$4.querySelector("#ykt-ai-pick-main-first");
    $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);
    ui.config.aiSlidePickPriority || "main";
    $priorityRadios.checked = ui.config.aiSlidePickMainFirst !== false;
    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);
      ui.config.aiSlidePickPriority = !!$priorityRadios.checked;
      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);
      ui.config.aiSlidePickPriority = DEFAULT_CONFIG.aiSlidePickPriority ?? true;
      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);
      $priorityRadios.checked = DEFAULT_CONFIG.aiSlidePickPriority ?? true;
      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">\r\n  <div class="panel-header">\r\n    <h3><i class="fas fa-robot"></i> AI 融合分析</h3>\r\n    <span id="ykt-ai-close" class="close-btn" title="关闭">\r\n      <i class="fas fa-times"></i>\r\n    </span>\r\n  </div>\r\n  <div class="panel-body">\r\n    <div style="margin-bottom: 10px;">\r\n      <strong>当前题目:</strong>\r\n      <div style="font-size: 12px; color: #666; margin: 4px 0;">\r\n        系统将自动识别当前页面的题目\r\n      </div>\r\n      <div id="ykt-ai-text-status" class="text-status warning">\r\n        正在检测题目信息...\r\n      </div>\r\n      <div id="ykt-ai-question-display" class="ykt-question-display">\r\n        提示:系统使用融合模式,同时分析题目文本信息和页面图像,提供最准确的答案。\r\n      </div>\r\n    </div>\r\n    \x3c!-- 当前要提问的PPT预览(来自presentation传入时显示) --\x3e\r\n    <div id="ykt-ai-selected" style="display:none; margin: 10px 0;">\r\n      <strong>已选PPT预览:</strong>\r\n      <div style="font-size: 12px; color: #666; margin: 4px 0;">\r\n        下方小图为即将用于分析的PPT页面截图\r\n      </div>\r\n      <div style="border: 1px solid var(--ykt-border-strong); padding: 6px; border-radius: 6px; display: inline-block;">\r\n        <img id="ykt-ai-selected-thumb"\r\n             alt="已选PPT预览"\r\n             style="max-width: 180px; max-height: 120px; display:block;" />\r\n      </div>\r\n    </div>\r\n    <div style="margin-bottom: 10px;">\r\n      <strong>自定义提示(可选):</strong>\r\n      <div style="font-size: 12px; color: #666; margin: 4px 0;">\r\n        提示:此内容将追加到系统生成的prompt后面,可用于补充特殊要求或背景信息。\r\n      </div>\r\n      <textarea \r\n        id="ykt-ai-custom-prompt" \r\n        class="ykt-custom-prompt"\r\n        placeholder="例如:请用中文回答、注重解题思路、考虑XXX知识点等"\r\n      ></textarea>\r\n    </div>\r\n\r\n    <button id="ykt-ai-ask" style="width: 100%; height: 32px; border-radius: 6px; border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer; margin-bottom: 10px;">\r\n      <i class="fas fa-brain"></i> 融合模式分析(文本+图像)\r\n    </button>\r\n\r\n    <div id="ykt-ai-loading" class="ai-loading" style="display: none;">\r\n      <i class="fas fa-spinner fa-spin"></i> AI正在使用融合模式分析...\r\n    </div>\r\n    <div id="ykt-ai-error" class="ai-error" style="display: none;"></div>\r\n    <div>\r\n      <strong>AI 分析结果:</strong>\r\n      <div id="ykt-ai-answer" class="ai-answer"></div>\r\n    </div>\r\n    \x3c!-- ✅ 新增:可编辑答案区(默认隐藏;当检测到题目并成功解析parsed时显示) --\x3e\r\n    <div id="ykt-ai-edit-section" style="display:none; margin-top:12px;">\r\n      <strong>提交前可编辑答案:</strong>\r\n      <div style="font-size: 12px; color: #666; margin: 4px 0;">\r\n        提示:这里是将要提交的“结构化答案”。可直接编辑。支持:\r\n        <br>• 选择题/投票:填写 <code>["A"]</code> 或 <code>A,B</code>\r\n        <br>• 填空题:填写 <code>[" 1"]</code> 或 直接写 <code> 1</code>(自动包成数组)\r\n        <br>• 主观题:可填 JSON(如 <code>{"content":"略","pics":[]}</code>)或直接输入文本\r\n      </div>\r\n      <textarea id="ykt-ai-answer-edit"\r\n        style="width:100%; min-height:88px; border:1px solid var(--ykt-border-strong); border-radius:6px; padding:6px; font-family:monospace;"></textarea>\r\n      <div id="ykt-ai-validate" style="font-size:12px; color:#666; margin-top:6px;"></div>\r\n      <div style="margin-top:8px; display:flex; gap:8px;">\r\n        <button id="ykt-ai-submit" class="ykt-btn ykt-btn-primary" style="flex:0 0 auto;">\r\n          提交编辑后的答案\r\n        </button>\r\n        <button id="ykt-ai-reset-edit" class="ykt-btn" style="flex:0 0 auto;">重置为 AI 建议</button>\r\n      </div>\r\n    </div>\r\n  </div>\r\n</div>';
  // src/ai/kimi.js
  // -----------------------------------------------
  // Unified Prompt blocks for Text & Vision
  // -----------------------------------------------
    const BASE_SYSTEM_PROMPT = [ "你是 Kimi,由 Moonshot AI 提供的人工智能助手。你需要在以下规则下工作:", "1) 任何时候优先遵循【用户输入(优先级最高)】中的明确要求;", "2) 当输入是课件页面(PPT)图像或题干文本时,先判断是否存在“明确题目”;", "3) 若存在明确题目,则输出以下格式的内容:", "   单选:格式要求:\n答案: [单个字母]\n解释: [选择理由]\n\n注意:只选一个,如A", "   多选:格式要求:\n答案: [多个字母用顿号分开]\n解释: [选择理由]\n\n注意:格式如A、B、C", "   投票:格式要求:\n答案: [单个字母]\n解释: [选择理由]\n\n注意:只选一个选项", "   填空/主观题: 格式要求:答案: [直接给出答案内容],解释: [简要说明]", "4) 若识别不到明确题目,直接使用回答用户输入的问题", "3) 如果PROMPT格式不正确,或者你只接收了图片,输出:", "   STATE: NO_PROMPT", "   SUMMARY: <介绍页面/上下文的主要内容>" ].join("\n");
  // Vision 补充:识别题型与版面元素的步骤说明
    const VISION_GUIDE = [ "【视觉识别要求】", "A. 先判断是否为题目页面(是否有题干/选项/空格/问句等)", "B. 若是题目,尝试提取题干、选项与关键信息;", "C. 否则参考用户输入回答" ].join("\n");
  /**
   * 调用 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,/, "");
    // 统一化:使用 BASE_SYSTEM_PROMPT + VISION_GUIDE,并要求先做“是否有题目”的决策
        const visionTextHeader = [ "【融合模式说明】你将看到一张课件/PPT截图与可选的附加文本。", VISION_GUIDE ].join("\n");
    // ✅ 按照文档要求构建消息格式
        const messages = [ {
      role: "system",
      content: BASE_SYSTEM_PROMPT
    }, {
      role: "user",
      content: [ {
        type: "image_url",
        image_url: {
          url: `data:image/png;base64,${cleanBase64}`
        }
      }, {
        type: "text",
        text: [ visionTextHeader, "【用户输入(优先级最高)】", textPrompt || "(无)" ].join("\n")
      } ]
    } ];
    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
      });
    });
  }
  // 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/panels/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";
    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-answer">\n          <div class="label">AI分析结果:</div>\n          <div class="content">${esc(aiAnswer || "无AI回答").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?.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;
    }
  }
  /**
   * ✅ 新方法:获取指定幻灯片的截图
   * @param {string} slideId - 幻灯片ID
   * @returns {Promise<string|null>} base64图片数据
   */  async function captureSlideImage(slideId) {
    try {
      console.log("[captureSlideImage] 获取幻灯片图片:", slideId);
      const slide = repo.slides.get(slideId);
      if (!slide) {
        console.error("[captureSlideImage] 找不到幻灯片:", slideId);
        return null;
      }
      // ✅ 使用 cover 或 coverAlt 图片URL
            const imageUrl = slide.coverAlt || slide.cover;
      if (!imageUrl) {
        console.error("[captureSlideImage] 幻灯片没有图片URL");
        return null;
      }
      console.log("[captureSlideImage] 图片URL:", imageUrl);
      // ✅ 下载图片并转换为base64
            const base64 = await downloadImageAsBase64(imageUrl);
      if (!base64) {
        console.error("[captureSlideImage] 下载图片失败");
        return null;
      }
      console.log("[captureSlideImage] ✅ 成功获取图片, 大小:", Math.round(base64.length / 1024), "KB");
      return base64;
    } catch (e) {
      console.error("[captureSlideImage] 失败:", e);
      return null;
    }
  }
  /**
   * ✅ 下载图片并转换为base64
   * @param {string} url - 图片URL
   * @returns {Promise<string|null>}
   */  async function downloadImageAsBase64(url) {
    return new Promise(resolve => {
      try {
        const img = new Image;
        img.crossOrigin = "anonymous";
 // ✅ 允许跨域
                img.onload = () => {
          try {
            const canvas = document.createElement("canvas");
            canvas.width = img.width;
            canvas.height = img.height;
            const ctx = canvas.getContext("2d");
            ctx.drawImage(img, 0, 0);
            // ✅ 转换为JPEG格式,压缩质量0.8
                        const base64 = canvas.toDataURL("image/jpeg", .8).split(",")[1];
            // ✅ 如果图片太大,进一步压缩
                        if (base64.length > 1e6) {
              // 1MB
              console.log("[downloadImageAsBase64] 图片过大,进行压缩...");
              const compressed = canvas.toDataURL("image/jpeg", .5).split(",")[1];
              console.log("[downloadImageAsBase64] 压缩后大小:", Math.round(compressed.length / 1024), "KB");
              resolve(compressed);
            } else resolve(base64);
          } catch (e) {
            console.error("[downloadImageAsBase64] Canvas处理失败:", e);
            resolve(null);
          }
        };
        img.onerror = e => {
          console.error("[downloadImageAsBase64] 图片加载失败:", e);
          resolve(null);
        };
        img.src = url;
      } catch (e) {
        console.error("[downloadImageAsBase64] 失败:", e);
        resolve(null);
      }
    });
  }
  // 原有的 captureProblemForVision 保留作为后备方案
    async function captureProblemForVision() {
    try {
      console.log("[captureProblemForVision] 开始截图...");
      const canvas = await captureProblemScreenshot();
      if (!canvas) {
        console.error("[captureProblemForVision] 截图失败");
        return null;
      }
      console.log("[captureProblemForVision] 截图成功,转换为base64...");
      const base64 = canvas.toDataURL("image/jpeg", .8).split(",")[1];
      console.log("[captureProblemForVision] base64 长度:", base64.length);
      if (base64.length > 1e6) {
        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;
    }
  }
  // src/tsm/ai-format.js
  // 预处理题目内容,去除题目类型标识
    function cleanProblemBody(body, problemType, TYPE_MAP) {
    if (!body) return "";
    const typeLabel = TYPE_MAP[problemType];
    if (!typeLabel) return body;
    // 去除题目开头的类型标识,如 "填空题:" "单选题:" 等
        const pattern = new RegExp(`^${typeLabel}[::\\s]+`, "i");
    return body.replace(pattern, "").trim();
  }
  // 改进的融合模式 prompt 格式化函数
    function formatProblemForVision(problem, TYPE_MAP, hasTextInfo = false) {
    const problemType = TYPE_MAP[problem.problemType] || "题目";
    let basePrompt = hasTextInfo ? `结合文本信息和图片内容分析${problemType},按格式回答:` : `观察图片内容,识别${problemType}并按格式回答:`;
    if (hasTextInfo && problem.body) {
      // ✅ 清理题目内容
      const cleanBody = cleanProblemBody(problem.body, problem.problemType, TYPE_MAP);
      basePrompt += `\n\n【文本信息】\n题目:${cleanBody}`;
      if (problem.options?.length) {
        basePrompt += "\n选项:";
        for (const o of problem.options) basePrompt += `\n${o.key}. ${o.value}`;
      }
      basePrompt += "\n\n若图片内容与文本冲突,以图片为准。";
    }
    // 根据题目类型添加具体格式要求
        switch (problem.problemType) {
     case 1:
      // 单选题
      basePrompt += `\n\n格式要求:\n答案: [单个字母]\n解释: [选择理由]\n\n注意:只选一个,如A`;
      break;

     case 2:
      // 多选题
      basePrompt += `\n\n格式要求:\n答案: [多个字母用顿号分开]\n解释: [选择理由]\n\n注意:格式如A、B、C`;
      break;

     case 3:
      // 投票题
      basePrompt += `\n\n格式要求:\n答案: [单个字母]\n解释: [选择理由]\n\n注意:只选一个选项`;
      break;

     case 4:
      // 填空题
      basePrompt += `\n\n这是一道填空题。\n\n重要说明:\n- 题目内容已经处理,不含"填空题"等字样\n- 观察图片和文本,找出需要填入的内容\n- 答案中不要出现任何题目类型标识\n\n格式要求:\n答案: [直接给出填空内容]\n解释: [简要说明]\n\n示例:\n答案: 氧气,葡萄糖\n解释: 光合作用的产物\n\n多个填空用逗号分开`;
      break;

     case 5:
      // 主观题
      basePrompt += `\n\n格式要求:\n答案: [完整回答]\n解释: [补充说明]\n\n注意:直接回答,不要重复题目`;
      break;

     default:
      basePrompt += `\n\n格式要求:\n答案: [你的答案]\n解释: [详细解释]`;
    }
    return basePrompt;
  }
  // 改进的答案解析函数
    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() || "";
      console.log("[parseAIAnswer] 题目类型:", problem.problemType, "原始答案行:", answerLine);
      switch (problem.problemType) {
       case 1:
 // 单选题
               case 3:
        {
          // 投票题
          let m = answerLine.match(/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]/);
          if (m) {
            console.log("[parseAIAnswer] 单选/投票解析结果:", [ m[0] ]);
            return [ m[0] ];
          }
          const chineseMatch = answerLine.match(/选择?([ABCDEFGHIJKLMNOPQRSTUVWXYZ])/);
          if (chineseMatch) {
            console.log("[parseAIAnswer] 单选/投票中文解析结果:", [ chineseMatch[1] ]);
            return [ chineseMatch[1] ];
          }
          console.log("[parseAIAnswer] 单选/投票解析失败");
          return null;
        }

       case 2:
        {
          // 多选题
          if (answerLine.includes("、")) {
            const options = answerLine.split("、").map(s => s.trim().match(/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]/)).filter(m => m).map(m => m[0]);
            if (options.length > 0) {
              const result = [ ...new Set(options) ].sort();
              console.log("[parseAIAnswer] 多选顿号解析结果:", result);
              return result;
            }
          }
          if (answerLine.includes(",") || answerLine.includes(",")) {
            const options = answerLine.split(/[,,]/).map(s => s.trim().match(/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]/)).filter(m => m).map(m => m[0]);
            if (options.length > 0) {
              const result = [ ...new Set(options) ].sort();
              console.log("[parseAIAnswer] 多选逗号解析结果:", result);
              return result;
            }
          }
          const letters = answerLine.match(/[ABCDEFGHIJKLMNOPQRSTUVWXYZ]/g);
          if (letters && letters.length > 1) {
            const result = [ ...new Set(letters) ].sort();
            console.log("[parseAIAnswer] 多选连续解析结果:", result);
            return result;
          }
          if (letters && letters.length === 1) {
            console.log("[parseAIAnswer] 多选单个解析结果:", letters);
            return letters;
          }
          console.log("[parseAIAnswer] 多选解析失败");
          return null;
        }

       case 4:
        {
          // 填空题
          // ✅ 更激进的清理策略
          let cleanAnswer = answerLine.replace(/^(填空题|简答题|问答题|题目|答案是?)[::\s]*/gi, "").trim();
          console.log("[parseAIAnswer] 清理后答案:", cleanAnswer);
          // 如果清理后还包含这些词,继续清理
                    if (/填空题|简答题|问答题|题目/i.test(cleanAnswer)) {
            cleanAnswer = cleanAnswer.replace(/填空题|简答题|问答题|题目/gi, "").trim();
            console.log("[parseAIAnswer] 二次清理后:", cleanAnswer);
          }
          const answerLength = cleanAnswer.length;
          if (answerLength <= 50) {
            cleanAnswer = cleanAnswer.replace(/^[^\w\u4e00-\u9fa5]+/, "").replace(/[^\w\u4e00-\u9fa5]+$/, "");
            const blanks = cleanAnswer.split(/[,,;;\s]+/).filter(Boolean);
            if (blanks.length > 0) {
              console.log("[parseAIAnswer] 填空解析结果:", blanks);
              return blanks;
            }
          }
          if (cleanAnswer) {
            const result = {
              content: cleanAnswer,
              pics: []
            };
            console.log("[parseAIAnswer] 简答题解析结果:", result);
            return result;
          }
          console.log("[parseAIAnswer] 填空/简答解析失败");
          return null;
        }

       case 5:
        {
          // 主观题
          const content = answerLine.replace(/^(主观题|论述题)[::\s]*/i, "").trim();
          if (content) {
            const result = {
              content: content,
              pics: []
            };
            console.log("[parseAIAnswer] 主观题解析结果:", result);
            return result;
          }
          console.log("[parseAIAnswer] 主观题解析失败");
          return null;
        }

       default:
        console.log("[parseAIAnswer] 未知题目类型:", problem.problemType);
        return null;
      }
    } catch (e) {
      console.error("[parseAIAnswer] 解析失败", e);
      return null;
    }
  }
  /**
   * Vuex 辅助工具 - 用于获取雨课堂主界面状态
   */
  /**
   * 获取 Vue 根实例
   * @returns {Vue | null}
   */  function getVueApp() {
    try {
      const app = document.querySelector("#app").__vue__;
      return app || null;
    } catch (e) {
      console.error("[getVueApp] 错误:", e);
      return null;
    }
  }
  /**
   * 从 Vuex state 获取主界面当前页面的 slideId
   * @returns {string | null}
   */  function getCurrentMainPageSlideId() {
    try {
      const app = getVueApp();
      if (!app || !app.$store) {
        console.log("[getCurrentMainPageSlideId] 无法获取 Vue 实例或 store");
        return null;
      }
      const currSlide = app.$store.state.currSlide;
      if (!currSlide || !currSlide.sid) {
        console.log("[getCurrentMainPageSlideId] currSlide 或 sid 未定义");
        return null;
      }
      console.log("[getCurrentMainPageSlideId] 获取到 slideId:", currSlide.sid, {
        type: currSlide.type,
        problemID: currSlide.problemID,
        index: currSlide.index
      });
      return currSlide.sid;
    } catch (e) {
      console.error("[getCurrentMainPageSlideId] 错误:", e);
      return null;
    }
  }
  /**
   * 监听主界面页面切换
   * @param {Function} callback - 回调函数 (slideId, slideInfo) => void
   * @returns {Function} - 取消监听的函数
   */  function watchMainPageChange(callback) {
    const app = getVueApp();
    if (!app || !app.$store) {
      console.error("[watchMainPageChange] 无法获取 Vue 实例");
      return () => {};
    }
    const unwatch = app.$store.watch(state => state.currSlide, (newSlide, oldSlide) => {
      if (newSlide && newSlide.sid) {
        console.log("[主界面页面切换]", {
          oldSid: oldSlide?.sid,
          newSid: newSlide.sid,
          type: newSlide.type,
          problemID: newSlide.problemID
        });
        callback(newSlide.sid, newSlide);
      }
    }, {
      deep: false
    });
    console.log("✅ 已启动主界面页面切换监听");
    return unwatch;
  }
  /**
   * 等待 Vue 实例准备就绪
   * @returns {Promise<Vue>}
   */  function waitForVueReady() {
    return new Promise(resolve => {
      const check = () => {
        const app = getVueApp();
        if (app && app.$store) resolve(app); else setTimeout(check, 100);
      };
      check();
    });
  }
  let mounted$4 = false;
  let root$3;
  // 来自 presentation 的优先提示(一次性优先使用)
    let preferredSlideFromPresentation = null;
  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));
    // 使用融合模式
        $$4("#ykt-ai-ask")?.addEventListener("click", askAIFusionMode);
    // ✅ 新增:启动主界面页面切换监听
        waitForVueReady().then(() => {
      watchMainPageChange((slideId, slideInfo) => {
        console.log("[AI Panel] 主界面页面切换到:", slideId);
        // 自动更新显示
                renderQuestion();
      });
    }).catch(e => {
      console.warn("[AI Panel] Vue 实例初始化失败,将使用备用方案:", e);
    });
    mounted$4 = true;
    return root$3;
  }
  window.addEventListener("ykt:open-ai", () => {
    showAIPanel(true);
  });
  // ✅ 来自 presentation 的“提问当前PPT”事件
    window.addEventListener("ykt:ask-ai-for-slide", ev => {
    const detail = ev?.detail || {};
    const {slideId: slideId, imageUrl: imageUrl} = detail;
    if (slideId) {
      preferredSlideFromPresentation = {
        slideId: slideId,
        imageUrl: imageUrl
      };
      // 若有 URL,直接覆盖 repo 内该页的 image,确保后续 capture 使用该 URL
            const s = repo.slides.get(slideId);
      if (s && imageUrl) s.image = imageUrl;
    }
    // 打开并刷新 UI + 预览
        showAIPanel(true);
    renderQuestion();
    const img = document.getElementById("ykt-ai-selected-thumb");
    const box = document.getElementById("ykt-ai-selected");
    if (img && box) {
      img.src = preferredSlideFromPresentation?.imageUrl || "";
      box.style.display = preferredSlideFromPresentation?.imageUrl ? "" : "none";
    }
  });
  function showAIPanel(visible = true) {
    mountAIPanel();
    root$3.classList.toggle("visible", !!visible);
    if (visible) {
      renderQuestion();
      if (ui.config.aiAutoAnalyze) queueMicrotask(() => {
        askAIFusionMode();
      });
    }
    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 || "";
  }
  // 新增:获取用户自定义prompt
    function getCustomPrompt() {
    const customPromptEl = $$4("#ykt-ai-custom-prompt");
    if (customPromptEl) {
      const customText = customPromptEl.value.trim();
      return customText || "";
    }
    return "";
  }
  function renderQuestion() {
    // ✅ 显示当前选择逻辑的状态
    let displayText = "";
    let hasPageSelected = false;
    let selectionSource = "";
    // 0. 若来自 presentation 的优先提示存在,则最高优先
        let slide = null;
    if (preferredSlideFromPresentation?.slideId) {
      slide = repo.slides.get(preferredSlideFromPresentation.slideId);
      if (slide) {
        displayText = `来自课件面板:${slide.title || `第 ${slide.page || slide.index || ""} 页`}`;
        selectionSource = "课件浏览(传入)";
        hasPageSelected = true;
      }
    }
    // 1. 若未命中优先提示,检查主界面
        if (!slide) {
      const prio = !!(ui?.config?.aiSlidePickPriority ?? true);
      if (prio) {
        const mainSlideId = getCurrentMainPageSlideId();
        slide = mainSlideId ? repo.slides.get(mainSlideId) : null;
        if (slide) {
          displayText = `主界面当前页: ${slide.title || `第 ${slide.page || slide.index || ""} 页`}`;
          selectionSource = "主界面检测";
          if (slide.problem) displayText += "\n📝 此页面包含题目"; else displayText += "\n📄 此页面为普通内容页";
          hasPageSelected = true;
        }
      } else {
        // 2. 检查课件面板选择
        const presentationPanel = document.getElementById("ykt-presentation-panel");
        const isPresentationPanelOpen = presentationPanel && presentationPanel.classList.contains("visible");
        if (isPresentationPanelOpen && repo.currentSlideId) {
          slide = repo.slides.get(repo.currentSlideId);
          if (slide) {
            displayText = `课件面板选中: ${slide.title || `第 ${slide.page || slide.index || ""} 页`}`;
            selectionSource = "课件浏览面板";
            hasPageSelected = true;
            if (slide.problem) displayText += "\n📝 此页面包含题目"; else displayText += "\n📄 此页面为普通内容页";
          }
        } else {
          displayText = `未检测到当前页面${presentationPanel}\n💡 请在课件面板(非侧边栏)中选择页面。`;
          selectionSource = "无";
        }
      }
    }
    const el = document.querySelector("#ykt-ai-question-display");
    if (el) el.textContent = displayText;
    // 同步预览块显示
        const img = document.getElementById("ykt-ai-selected-thumb");
    const box = document.getElementById("ykt-ai-selected");
    if (img && box) if (preferredSlideFromPresentation?.imageUrl) {
      img.src = preferredSlideFromPresentation.imageUrl;
      box.style.display = "";
    } else box.style.display = "none";
    const statusEl = document.querySelector("#ykt-ai-text-status");
    if (statusEl) {
      statusEl.textContent = hasPageSelected ? `✓ 已选择页面(来源:${selectionSource}),可进行图像分析` : "⚠ 请选择要分析的页面";
      statusEl.className = hasPageSelected ? "text-status success" : "text-status warning";
    }
  }
  // 融合模式AI询问函数(仅图像分析)- 支持自定义prompt
    async function askAIFusionMode() {
    setAIError("");
    setAILoading(true);
    setAIAnswer("");
    try {
      if (!ui.config.ai.kimiApiKey) throw new Error("请先在设置中配置 Kimi API Key");
      // ✅ 智能选择当前页面:优先“presentation 传入”,其后主界面、最后课件面板
            let currentSlideId = null;
      let slide = null;
      let selectionSource = "";
      let forcedImageUrl = null;
      // 0) 优先使用 presentation 传入的 slide
            if (preferredSlideFromPresentation?.slideId) {
        currentSlideId = preferredSlideFromPresentation.slideId;
        slide = repo.slides.get(currentSlideId);
        forcedImageUrl = preferredSlideFromPresentation.imageUrl || null;
        selectionSource = "课件浏览(传入)";
        console.log("[AI Panel] 使用presentation传入的页面:", currentSlideId);
      }
      // 1) 其后:主界面当前页面
            if (!slide) {
        const prio = !!(ui?.config?.aiSlidePickPriority ?? true);
        if (prio) {
          const mainSlideId = getCurrentMainPageSlideId();
          if (mainSlideId) {
            currentSlideId = mainSlideId;
            slide = repo.slides.get(currentSlideId);
            selectionSource = "主界面当前页面";
            console.log("[AI Panel] 使用主界面当前页面:", currentSlideId);
          }
        } else {
          const presentationPanel = document.getElementById("ykt-presentation-panel");
          const isPresentationPanelOpen = presentationPanel && presentationPanel.classList.contains("visible");
          if (isPresentationPanelOpen && repo.currentSlideId) {
            currentSlideId = repo.currentSlideId;
            slide = repo.slides.get(currentSlideId);
            selectionSource = "课件浏览面板";
            console.log("[AI Panel] 使用课件面板选中的页面:", currentSlideId);
          }
        }
      }
      // 3. 检查是否成功获取到页面
            if (!currentSlideId || !slide) throw new Error("无法确定要分析的页面。请在主界面打开一个页面,或在课件浏览中选择页面。");
      console.log("[AI Panel] 页面选择来源:", selectionSource);
      console.log("[AI Panel] 分析页面ID:", currentSlideId);
      console.log("[AI Panel] 页面信息:", slide);
      // ✅ 直接使用选中页面的图片
            console.log("[AI Panel] 获取页面图片...");
      ui.toast(`正在获取${selectionSource}图片...`, 2e3);
      let imageBase64 = null;
      // 若 presentation 传入了 URL,则优先用该 URL(captureSlideImage 会读 slide.image)
            if (forcedImageUrl) 
      // 确保 slide.image 是这张图,captureSlideImage 将基于 slideId 取图
      if (slide) slide.image = forcedImageUrl;
      imageBase64 = await captureSlideImage(currentSlideId);
      if (!imageBase64) throw new Error("无法获取页面图片,请确保页面已加载完成");
      console.log("[AI Panel] ✅ 页面图片获取成功");
      console.log("[AI Panel] 图像大小:", Math.round(imageBase64.length / 1024), "KB");
      // ✅ 构建纯图像分析提示(不使用题目文本)
            let textPrompt = `【页面说明】当前页面可能不是题目页;请结合用户提示作答。`;
      // 获取用户自定义prompt并追加
            const customPrompt = getCustomPrompt();
      if (customPrompt) {
        textPrompt += `\n\n【用户自定义要求】\n${customPrompt}`;
        console.log("[AI Panel] 用户添加了自定义prompt:", customPrompt);
      }
      ui.toast(`正在分析${selectionSource}内容...`, 3e3);
      console.log("[AI Panel] 调用Vision API...");
      console.log("[AI Panel] 使用的提示:", textPrompt);
      const aiContent = await queryKimiVision(imageBase64, textPrompt, ui.config.ai);
      setAILoading(false);
      console.log("[AI Panel] Vision API调用成功");
      console.log("[AI Panel] AI回答:", aiContent);
      // ✅ 尝试解析答案(如果当前页面有题目的话)
            let parsed = null;
      const problem = slide?.problem;
      if (problem) {
        parsed = parseAIAnswer(problem, aiContent);
        console.log("[AI Panel] 解析结果:", parsed);
      }
      // 构建显示内容
            let displayContent = `${selectionSource}图像分析结果:\n${aiContent}`;
      if (customPrompt) displayContent = `${selectionSource}图像分析结果(包含自定义要求):\n${aiContent}`;
      if (parsed && problem) {
        setAIAnswer(`${displayContent}\n\nAI 建议答案:${JSON.stringify(parsed)}`);
        // // ✅ 只有当前页面有题目时才显示提交按钮
        // const submitBtn = document.createElement('button');
        // submitBtn.textContent = '提交答案';
        // submitBtn.className = 'ykt-btn ykt-btn-primary';
        // submitBtn.onclick = async () => {
        //   try {
        //     if (!problem || !problem.problemId) {
        //       ui.toast('当前页面没有可提交的题目');
        //       return;
        //     }
        //     console.log('[AI Panel] 准备提交答案');
        //     console.log('[AI Panel] Problem:', problem);
        //     console.log('[AI Panel] Parsed:', parsed);
        //     await submitAnswer(problem, parsed);
        //     ui.toast('提交成功');
        //     showAutoAnswerPopup(problem, aiContent);
        //   } catch (e) {
        //     console.error('[AI Panel] 提交失败:', e);
        //     ui.toast(`提交失败: ${e.message}`);
        //   }
        // };
        // $('#ykt-ai-answer').appendChild(document.createElement('br'));
        // $('#ykt-ai-answer').appendChild(submitBtn);
        // ✅ 改为:显示“可编辑答案区”,预填 parsed,并提供“提交编辑后的答案”
                const editBox = $$4("#ykt-ai-answer-edit");
        const editSec = $$4("#ykt-ai-edit-section");
        const submitBtn = $$4("#ykt-ai-submit");
        const resetBtn = $$4("#ykt-ai-reset-edit");
        const validEl = $$4("#ykt-ai-validate");
        if (editBox && editSec && submitBtn && resetBtn) {
          editSec.style.display = "";
          const aiSuggested = JSON.stringify(parsed);
          editBox.value = aiSuggested;
          validEl.textContent = "已载入 AI 建议答案,可编辑后提交。";
          // 变化时做一次轻量校验提示
                    editBox.oninput = () => {
            try {
              // 尝试 JSON;失败也不报红,交由 coerceEditedAnswer 兜底
              JSON.parse(editBox.value);
              validEl.textContent = "解析正常(JSON)。";
              validEl.style.color = "#2a6";
            } catch {
              validEl.textContent = "非 JSON,将按题型做容错解析。";
              validEl.style.color = "#666";
            }
          };
          submitBtn.onclick = async () => {
            try {
              if (!problem?.problemId) {
                ui.toast("当前页面没有可提交的题目");
                return;
              }
              // ✅ 关键修复:先尝试把文本解析为 JSON;失败则回退到 parsed(结构化对象/数组)
                            const raw = (editBox.value || "").trim();
              let payload = null;
              try {
                payload = raw ? JSON.parse(raw) : null;
              } catch {
                payload = null;
              }
              if (payload == null) payload = parsed;
 // 回退
                            console.log("[AI Panel] 准备提交(编辑后):", payload);
              await submitAnswer(problem, payload);
              ui.toast("提交成功");
              showAutoAnswerPopup(problem, aiContent);
            } catch (e) {
              console.error("[AI Panel] 提交失败:", e);
              ui.toast(`提交失败: ${e.message}`);
            }
          };
          resetBtn.onclick = () => {
            editBox.value = aiSuggested;
            validEl.textContent = "已重置为 AI 建议答案。";
            validEl.style.color = "#666";
          };
        }
      } else {
        // ✅ 如果当前页面没有题目,告知用户
        if (!problem) displayContent += "\n\n💡 当前页面不是题目页面(或未识别到题目)。若要提问,请在上方输入框中补充你的问题(已被最高优先级处理)。"; else displayContent += "\n\n⚠️ 无法自动解析答案格式,请检查AI回答是否符合要求格式。";
        setAIAnswer(displayContent);
      }
    } catch (e) {
      setAILoading(false);
      console.error("[AI Panel] 页面分析失败:", e);
      // 失败后不清除 preferred,便于用户修正后重试
            let errorMsg = `页面分析失败: ${e.message}`;
      if (e.message.includes("400")) errorMsg += "\n\n可能的解决方案:\n1. 检查 API Key 是否正确\n2. 尝试刷新页面后重试\n3. 确保页面已完全加载";
      setAIError(errorMsg);
    }
  }
  /**
   * 获取主界面当前显示的页面ID
   * @returns {string|null} 当前页面的slideId
   */
  // function getCurrentMainPageSlideId() {
  //   try {
  //     // 方法1:从当前最近遇到的问题获取(最可能是当前页面)
  //     if (repo.encounteredProblems.length > 0) {
  //       const latestProblem = repo.encounteredProblems.at(-1);
  //       const problemStatus = repo.problemStatus.get(latestProblem.problemId);
  //       if (problemStatus && problemStatus.slideId) {
  //         console.log('[getCurrentMainPageSlideId] 从最近问题获取:', problemStatus.slideId);
  //         return problemStatus.slideId;
  //       }
  //     }
  //     // 方法2:从DOM结构尝试获取(雨课堂可能的DOM结构)
  //     const slideElements = [
  //       document.querySelector('[data-slide-id]'),
  //       document.querySelector('.slide-wrapper.active'),
  //       document.querySelector('.ppt-slide.active'),
  //       document.querySelector('.current-slide')
  //     ];
  //     for (const el of slideElements) {
  //       if (el) {
  //         const slideId = el.dataset?.slideId || el.getAttribute('data-slide-id');
  //         if (slideId) {
  //           console.log('[getCurrentMainPageSlideId] 从DOM获取:', slideId);
  //           return slideId;
  //         }
  //       }
  //     }
  //     // 方法3:如果没有找到,返回null
  //     console.log('[getCurrentMainPageSlideId] 无法获取主界面当前页面');
  //     return null;
  //   } catch (e) {
  //     console.error('[getCurrentMainPageSlideId] 获取失败:', e);
  //     return null;
  //   }
  // }
  // 保留其他函数以向后兼容,但现在都指向融合模式
    async function askAIForCurrent() {
    return askAIFusionMode();
  }
  var tpl$3 = '<div id="ykt-presentation-panel" class="ykt-panel">\r\n  <div class="panel-header">\r\n    <h3>课件浏览</h3>\r\n    <div class="panel-controls">\r\n      <label>\r\n        <input type="checkbox" id="ykt-show-all-slides"> 切换全部页面/问题页面\r\n      </label>\r\n      <button id="ykt-ask-current">提问当前PPT</button>\r\n      <button id="ykt-open-problem-list">题目列表</button>\r\n      <button id="ykt-download-current">截图下载</button>\r\n      <button id="ykt-download-pdf">整册下载(PDF)</button>\r\n      <span class="close-btn" id="ykt-presentation-close"><i class="fas fa-times"></i></span>\r\n    </div>\r\n  </div>\r\n\r\n  <div class="panel-body">\r\n    <div class="panel-left">\r\n      <div id="ykt-presentation-list" class="presentation-list"></div>\r\n    </div>\r\n    <div class="panel-right">\r\n      <div id="ykt-slide-view" class="slide-view">\r\n        <div class="slide-cover">\r\n          <div class="empty-message">选择左侧的幻灯片查看详情</div>\r\n        </div>\r\n        <div id="ykt-problem-view" class="problem-view"></div>\r\n      </div>\r\n    </div>\r\n  </div>\r\n</div>\r\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"));
    });
    // 1.18.4: 提问当前PPT:把当前 slide 信息传给 AI 面板
        $$3("#ykt-ask-current")?.addEventListener("click", () => {
      if (!repo.currentSlideId) return ui.toast("请先在左侧选择一页PPT", 2500);
      const slide = repo.slides.get(repo.currentSlideId);
      const imageUrl = slide?.image || slide?.thumbnail || "";
      // 通知 AI 面板:优先使用传入的 slide 和 URL
            window.dispatchEvent(new CustomEvent("ykt:ask-ai-for-slide", {
        detail: {
          slideId: repo.currentSlideId,
          imageUrl: imageUrl
        }
      }));
      // 打开 AI 面板
            window.dispatchEvent(new CustomEvent("ykt:open-ai"));
    });
    $$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">\r\n  <div class="panel-header">\r\n    <h3>课堂习题列表</h3>\r\n    <span class="close-btn" id="ykt-problem-list-close"><i class="fas fa-times"></i></span>\r\n  </div>\r\n\r\n  <div class="panel-body">\r\n    <div id="ykt-problem-list" class="problem-list">\r\n      \x3c!-- 由 problem-list.js 动态填充:\r\n           .problem-row\r\n             .problem-title\r\n             .problem-meta\r\n             .problem-actions (查看 / AI解答 / 已作答) --\x3e\r\n    </div>\r\n  </div>\r\n</div>\r\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">\r\n  <div id="ykt-active-problems" class="active-problems"></div>\r\n</div>\r\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();
    let hasActiveProblems = false;
 // ✅ 跟踪是否有活跃题目
        repo.problemStatus.forEach((status, pid) => {
      const p = repo.problems.get(pid);
      if (!p || p.result) return;
      const remain = Math.max(0, Math.floor((status.endTime - now) / 1e3));
      // ✅ 如果倒计时结束(剩余时间为0),跳过显示这个卡片
            if (remain <= 0) {
        console.log(`[ActiveProblems] 题目 ${pid} 倒计时已结束,移除卡片`);
        return;
      }
      // ✅ 有至少一个活跃题目
            hasActiveProblems = true;
      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 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);
    });
    // ✅ 如果没有活跃题目,隐藏整个面板容器
        if (!hasActiveProblems) root$1.style.display = "none"; else root$1.style.display = "";
  }
  var tpl = '<div id="ykt-tutorial-panel" class="ykt-panel">\r\n  <div class="panel-header">\r\n    <h3>雨课堂助手使用教程</h3>\r\n    <h5>1.18.5</h5>\r\n    <span class="close-btn" id="ykt-tutorial-close"><i class="fas fa-times"></i></span>\r\n  </div>\r\n\r\n  <div class="panel-body">\r\n    <div class="tutorial-content">\r\n      <h4>功能介绍</h4>\r\n      <p>AI雨课堂助手是一个为雨课堂提供辅助功能的工具,可以帮助你更好地参与课堂互动。</p>\r\n      <p>项目仓库:<a href="https://github.com/ZaytsevZY/yuketang-helper-auto" target="_blank" rel="noopener">GitHub</a></p>\r\n      <p>脚本安装:<a href="http://greasyfork.icu/zh-CN/scripts/531469-ai%E9%9B%A8%E8%AF%BE%E5%A0%82%E5%8A%A9%E6%89%8B-%E6%A8%A1%E5%9D%97%E5%8C%96%E6%9E%84%E5%BB%BA%E7%89%88" target="_blank" rel="noopener">GreasyFork</a></p>\r\n\r\n      <h4>工具栏按钮说明</h4>\r\n      <ul>\r\n        <li><i class="fas fa-bell"></i> <b>习题提醒</b>:切换是否在新习题出现时显示通知提示(蓝色=开启)。</li>\r\n        <li><i class="fas fa-file-powerpoint"></i> <b>课件浏览</b>:查看课件与题目页面,提问可见内容。</li>\r\n        <li><i class="fas fa-robot"></i> <b>AI 解答</b>:向 AI 询问当前题目并显示建议答案。</li>\r\n        <li><i class="fas fa-magic-wand-sparkles"></i> <b>自动作答</b>:切换自动作答(蓝色=开启)。</li>\r\n        <li><i class="fas fa-cog"></i> <b>设置</b>:配置 API 密钥与自动作答参数。</li>\r\n        <li><i class="fas fa-question-circle"></i> <b>使用教程</b>:显示/隐藏当前教程页面。</li>\r\n      </ul>\r\n\r\n      <h4>自动作答</h4>\r\n      <ul>\r\n        <li>在设置中开启自动作答并配置延迟/随机延迟。</li>\r\n        <li>需要配置 <del>DeepSeek API</del> Kimi API 密钥。</li>\r\n        <li>答案来自 AI,结果仅供参考。</li>\r\n      </ul>\r\n\r\n      <h4>AI 解答</h4>\r\n      <ol>\r\n        <li>点击设置(<i class="fas fa-cog"></i>)填入 API Key。</li>\r\n        <li>点击 AI 解答(<i class="fas fa-robot"></i>)后会对“当前题目/最近遇到的题目”询问并解析。</li>\r\n      </ol>\r\n\r\n      <h4>注意事项</h4>\r\n      <p>1) 仅供学习参考,请独立思考;</p>\r\n      <p>2) 合理使用 API 额度;</p>\r\n      <p>3) 答案不保证 100% 正确;</p>\r\n      <p>4) 自动作答有一定风险,谨慎开启。</p>\r\n\r\n      <h4>联系方式</h4>\r\n      <ul>\r\n        <li>请在Github issue提出问题</li>\r\n      </ul>\r\n    </div>\r\n  </div>\r\n</div>\r\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: 5000,
      // });
    },
    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
    let _autoLoopStarted = false;
  // 1.18.5: 本地默认答案生成(无 API Key 时使用,保持 AutoAnswer 流程通畅)
    function makeDefaultAnswer(problem) {
    switch (problem.problemType) {
     case 1:
 // 单选
           case 2:
 // 多选
           case 3:
      // 投票
      return [ "A" ];

     case 4:
      // 填空
      // 按需求示例返回 [" 1"](保留前导空格)
      return [ " 1" ];

     case 5:
      // 主观/问答
      return {
        content: "略",
        pics: []
      };

     default:
      // 兜底:按单选处理
      return [ "A" ];
    }
  }
  // 内部自动答题处理函数 - 融合模式(文本+图像)
    async function handleAutoAnswerInternal(problem) {
    const status = repo.problemStatus.get(problem.problemId);
    if (!status || status.answering || problem.result) {
      console.log("[AutoAnswer] 跳过:", {
        hasStatus: !!status,
        answering: status?.answering,
        hasResult: !!problem.result
      });
      return;
    }
    if (Date.now() >= status.endTime) {
      console.log("[AutoAnswer] 跳过:已超时");
      return;
    }
    status.answering = true;
    try {
      console.log("[AutoAnswer] =================================");
      console.log("[AutoAnswer] 开始自动答题");
      console.log("[AutoAnswer] 题目ID:", problem.problemId);
      console.log("[AutoAnswer] 题目类型:", PROBLEM_TYPE_MAP[problem.problemType]);
      console.log("[AutoAnswer] 题目内容:", problem.body?.slice(0, 50) + "...");
      if (!ui.config.ai.kimiApiKey) {
        // ✅ 无 API Key:使用本地默认答案直接提交,确保流程不中断
        const parsed = makeDefaultAnswer(problem);
        console.log("[AutoAnswer] 无 API Key,使用本地默认答案:", JSON.stringify(parsed));
        // 提交答案(根据时限自动选择 answer/retry 逻辑)
                await submitAnswer(problem, parsed, {
          startTime: status.startTime,
          endTime: status.endTime,
          forceRetry: false
        });
        // 更新状态与UI
                actions.onAnswerProblem(problem.problemId, parsed);
        status.done = true;
        status.answering = false;
        ui.toast("✅ 使用默认答案完成作答(未配置 API Key)", 3e3);
        showAutoAnswerPopup(problem, "(本地默认答案:无 API Key)");
        console.log("[AutoAnswer] ✅ 默认答案提交流程结束");
        return;
 // 提前返回,避免继续走图像+AI流程
            }
      const slideId = status.slideId;
      console.log("[AutoAnswer] 题目所在幻灯片:", slideId);
      console.log("[AutoAnswer] =================================");
      // ✅ 关键修复:直接使用幻灯片的cover图片,而不是截图DOM
            console.log("[AutoAnswer] 使用融合模式分析(文本+幻灯片图片)...");
      const imageBase64 = await captureSlideImage(slideId);
      // ✅ 如果获取幻灯片图片失败,回退到DOM截图
            if (!imageBase64) {
        console.log("[AutoAnswer] 无法获取幻灯片图片,尝试使用DOM截图...");
        const fallbackImage = await captureProblemForVision();
        if (!fallbackImage) {
          status.answering = false;
          console.error("[AutoAnswer] 所有截图方法都失败");
          return ui.toast("无法获取题目图像,跳过自动作答", 3e3);
        }
        imageBase64 = fallbackImage;
        console.log("[AutoAnswer] ✅ DOM截图成功");
      } else console.log("[AutoAnswer] ✅ 幻灯片图片获取成功");
      console.log("[AutoAnswer] 图片大小:", Math.round(imageBase64.length / 1024), "KB");
      // 构建提示
            const hasTextInfo = problem.body && problem.body.trim();
      const textPrompt = formatProblemForVision(problem, PROBLEM_TYPE_MAP, hasTextInfo);
      console.log("[AutoAnswer] 文本信息:", hasTextInfo ? "有" : "无");
      console.log("[AutoAnswer] 提示长度:", textPrompt.length);
      // 调用 AI
            ui.toast("AI 正在分析题目...", 2e3);
      const aiAnswer = await queryKimiVision(imageBase64, textPrompt, ui.config.ai);
      console.log("[AutoAnswer] ✅ AI回答:", aiAnswer);
      // 解析答案
            const parsed = parseAIAnswer(problem, aiAnswer);
      console.log("[AutoAnswer] 解析结果:", parsed);
      if (!parsed) {
        status.answering = false;
        console.error("[AutoAnswer] 解析失败,AI回答格式不正确");
        return ui.toast("无法解析AI答案,请检查格式", 3e3);
      }
      console.log("[AutoAnswer] ✅ 准备提交答案:", JSON.stringify(parsed));
      // 提交答案
            await submitAnswer(problem, parsed, {
        startTime: status.startTime,
        endTime: status.endTime,
        forceRetry: false
      });
      console.log("[AutoAnswer] ✅ 提交成功");
      // 更新状态
            actions.onAnswerProblem(problem.problemId, parsed);
      status.done = true;
      status.answering = false;
      ui.toast(`✅ 自动作答完成`, 3e3);
      showAutoAnswerPopup(problem, aiAnswer);
    } catch (e) {
      console.error("[AutoAnswer] ❌ 失败:", e);
      console.error("[AutoAnswer] 错误堆栈:", e.stack);
      status.answering = false;
      ui.toast(`自动作答失败: ${e.message}`, 4e3);
    }
  }
  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) {
        console.log("[onUnlockProblem] 题目或幻灯片不存在");
        return;
      }
      console.log("[onUnlockProblem] 题目解锁");
      console.log("[onUnlockProblem] 题目ID:", data.prob);
      console.log("[onUnlockProblem] 幻灯片ID:", data.sid);
      console.log("[onUnlockProblem] 课件ID:", data.pres);
      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) {
        console.log("[onUnlockProblem] 题目已过期或已作答,跳过");
        return;
      }
      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;
        console.log(`[onUnlockProblem] 将在 ${Math.floor(delay / 1e3)} 秒后自动作答`);
        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);
    },
    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);
    },
    launchLessonHelper() {
      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}`);
      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();
    },
    startAutoAnswerLoop() {
      if (_autoLoopStarted) return;
      _autoLoopStarted = true;
      setInterval(() => {
        const now = Date.now();
        repo.problemStatus.forEach((status, pid) => {
          if (status.autoAnswerTime !== null && now >= status.autoAnswerTime) {
            const problem = repo.problems.get(pid);
            if (problem && !problem.result) {
              status.autoAnswerTime = null;
              handleAutoAnswerInternal(problem);
            }
          }
        });
      }, 500);
    }
  };
  // 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);
          }
          // 监听后端传递的url
                    const url = function findUrl(obj) {
            if (!obj || typeof obj !== "object") return null;
            if (typeof obj.url === "string") return obj.url;
            if (Array.isArray(obj)) for (const it of obj) {
              const u = findUrl(it);
              if (u) return u;
            } else for (const k in obj) {
              const v = obj[k];
              if (v && typeof v === "object") {
                const u = findUrl(v);
                if (u) return u;
              }
            }
            return null;
          }(message);
          if (url) {
            window.dispatchEvent(new CustomEvent("ykt:url-change", {
              detail: {
                url: url,
                raw: message
              }
            }));
            // 如需持久化到 repo,请取消下一行注释(确保已在 repo 定义该字段)
                        repo.currentSelectedUrl = url;
            console.debug("[雨课堂助手] 当前选择 URL:", url);
          }
        } 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 = '/* ===== 通用 & 修复 ===== */\r\n#watermark_layer { display: none !important; visibility: hidden !important; }\r\n.hidden { display: none !important; }\r\n\r\n:root{\r\n  --ykt-z: 10000000;\r\n  --ykt-border: #ddd;\r\n  --ykt-border-strong: #ccc;\r\n  --ykt-bg: #fff;\r\n  --ykt-fg: #222;\r\n  --ykt-muted: #607190;\r\n  --ykt-accent: #1d63df;\r\n  --ykt-hover: #1e3050;\r\n  --ykt-shadow: 0 10px 30px rgba(0,0,0,.18);\r\n}\r\n\r\n/* ===== 工具栏 ===== */\r\n#ykt-helper-toolbar{\r\n  position: fixed; z-index: calc(var(--ykt-z) + 1);\r\n  left: 15px; bottom: 15px;\r\n  /* 移除固定宽度,让内容自适应 */\r\n  height: 36px; padding: 5px;\r\n  display: flex; gap: 6px; align-items: center;\r\n  background: var(--ykt-bg);\r\n  border: 1px solid var(--ykt-border-strong);\r\n  border-radius: 4px;\r\n  box-shadow: 0 1px 4px 3px rgba(0,0,0,.1);\r\n}\r\n\r\n#ykt-helper-toolbar .btn{\r\n  display: inline-block; padding: 4px; cursor: pointer;\r\n  color: var(--ykt-muted); line-height: 1;\r\n}\r\n#ykt-helper-toolbar .btn:hover{ color: var(--ykt-hover); }\r\n#ykt-helper-toolbar .btn.active{ color: var(--ykt-accent); }\r\n\r\n/* ===== 面板通用样式 ===== */\r\n.ykt-panel{\r\n  position: fixed; right: 20px; bottom: 60px;\r\n  width: 560px; max-height: 72vh; overflow: auto;\r\n  background: var(--ykt-bg); color: var(--ykt-fg);\r\n  border: 1px solid var(--ykt-border-strong); border-radius: 8px;\r\n  box-shadow: var(--ykt-shadow);\r\n  display: none; \r\n  /* 提高z-index,确保后打开的面板在最上层 */\r\n  z-index: var(--ykt-z);\r\n}\r\n.ykt-panel.visible{ \r\n  display: block; \r\n  /* 动态提升z-index */\r\n  z-index: calc(var(--ykt-z) + 10);\r\n}\r\n\r\n.panel-header{\r\n  display: flex; align-items: center; justify-content: space-between;\r\n  gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--ykt-border);\r\n}\r\n.panel-header h3{ margin: 0; font-size: 16px; font-weight: 600; }\r\n.panel-body{ padding: 10px 12px; }\r\n.close-btn{ cursor: pointer; color: var(--ykt-muted); }\r\n.close-btn:hover{ color: var(--ykt-hover); }\r\n\r\n/* ===== 设置面板 (#ykt-settings-panel) ===== */\r\n#ykt-settings-panel .settings-content{ display: flex; flex-direction: column; gap: 14px; }\r\n#ykt-settings-panel .setting-group{ border: 1px dashed var(--ykt-border); border-radius: 6px; padding: 10px; }\r\n#ykt-settings-panel .setting-group h4{ margin: 0 0 8px 0; font-size: 14px; }\r\n#ykt-settings-panel .setting-item{ display: flex; align-items: center; gap: 8px; margin: 8px 0; flex-wrap: wrap; }\r\n#ykt-settings-panel label{ font-size: 13px; }\r\n#ykt-settings-panel input[type="text"],\r\n#ykt-settings-panel input[type="number"]{\r\n  height: 30px; border: 1px solid var(--ykt-border-strong);\r\n  border-radius: 4px; padding: 0 8px; min-width: 220px;\r\n}\r\n#ykt-settings-panel small{ color: #666; }\r\n#ykt-settings-panel .setting-actions{ display: flex; gap: 8px; margin-top: 6px; }\r\n#ykt-settings-panel button{\r\n  height: 30px; padding: 0 12px; border-radius: 6px;\r\n  border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer;\r\n}\r\n#ykt-settings-panel button:hover{ background: #eef3ff; border-color: var(--ykt-accent); }\r\n\r\n/* 自定义复选框(与手写脚本一致的视觉语义) */\r\n#ykt-settings-panel .checkbox-label{ position: relative; padding-left: 26px; cursor: pointer; user-select: none; }\r\n#ykt-settings-panel .checkbox-label input{ position: absolute; opacity: 0; cursor: pointer; height: 0; width: 0; }\r\n#ykt-settings-panel .checkbox-label .checkmark{\r\n  position: absolute; left: 0; top: 50%; transform: translateY(-50%);\r\n  height: 16px; width: 16px; border:1px solid var(--ykt-border-strong); border-radius: 3px; background: #fff;\r\n}\r\n#ykt-settings-panel .checkbox-label input:checked ~ .checkmark{\r\n  background: var(--ykt-accent); border-color: var(--ykt-accent);\r\n}\r\n#ykt-settings-panel .checkbox-label .checkmark:after{\r\n  content: ""; position: absolute; display: none;\r\n  left: 5px; top: 1px; width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);\r\n}\r\n#ykt-settings-panel .checkbox-label input:checked ~ .checkmark:after{ display: block; }\r\n\r\n/* ===== AI 解答面板 (#ykt-ai-answer-panel) ===== */\r\n#ykt-ai-answer-panel .ai-question{\r\n  white-space: pre-wrap; background: #fafafa; border: 1px solid var(--ykt-border);\r\n  padding: 8px; border-radius: 6px; margin-bottom: 8px; max-height: 160px; overflow: auto;\r\n}\r\n#ykt-ai-answer-panel .ai-loading{ color: var(--ykt-accent); margin-bottom: 6px; }\r\n#ykt-ai-answer-panel .ai-error{ color: #b00020; margin-bottom: 6px; }\r\n#ykt-ai-answer-panel .ai-answer{ white-space: pre-wrap; margin-top: 4px; }\r\n#ykt-ai-answer-panel .ai-actions{ margin-top: 10px; }\r\n#ykt-ai-answer-panel .ai-actions button{\r\n  height: 30px; padding: 0 12px; border-radius: 6px;\r\n  border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer;\r\n}\r\n#ykt-ai-answer-panel .ai-actions button:hover{ background: #eef3ff; border-color: var(--ykt-accent); }\r\n\r\n/* ===== 课件浏览面板 (#ykt-presentation-panel) ===== */\r\n#ykt-presentation-panel{ width: 900px; }\r\n#ykt-presentation-panel .panel-controls{ display: flex; align-items: center; gap: 8px; }\r\n#ykt-presentation-panel .panel-body{\r\n  display: grid; grid-template-columns: 300px 1fr; gap: 10px;\r\n}\r\n#ykt-presentation-panel .presentation-title{\r\n  font-weight: 600; padding: 6px 0; border-bottom: 1px solid var(--ykt-border);\r\n}\r\n#ykt-presentation-panel .slide-thumb-list{ display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }\r\n#ykt-presentation-panel .slide-thumb{\r\n  border: 1px solid var(--ykt-border); border-radius: 6px; background: #fafafa;\r\n  min-height: 60px; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 4px; text-align: center;\r\n}\r\n#ykt-presentation-panel .slide-thumb:hover{ border-color: var(--ykt-accent); background: #eef3ff; }\r\n#ykt-presentation-panel .slide-thumb img{ max-width: 100%; max-height: 120px; object-fit: contain; display: block; }\r\n\r\n#ykt-presentation-panel .slide-view{\r\n  position: relative; border: 1px solid var(--ykt-border); border-radius: 8px; min-height: 360px; background: #fff; overflow: hidden;\r\n}\r\n#ykt-presentation-panel .slide-cover{ display: flex; align-items: center; justify-content: center; min-height: 360px; }\r\n#ykt-presentation-panel .slide-cover img{ max-width: 100%; max-height: 100%; object-fit: contain; display: block; }\r\n\r\n#ykt-presentation-panel .problem-box{\r\n  position: absolute; left: 12px; right: 12px; bottom: 12px;\r\n  background: rgba(255,255,255,.96); border: 1px solid var(--ykt-border);\r\n  border-radius: 8px; padding: 10px; box-shadow: 0 6px 18px rgba(0,0,0,.12);\r\n}\r\n#ykt-presentation-panel .problem-head{ font-weight: 600; margin-bottom: 6px; }\r\n#ykt-presentation-panel .problem-options{ display: grid; grid-template-columns: 1fr; gap: 4px; }\r\n#ykt-presentation-panel .problem-option{ padding: 6px 8px; border: 1px solid var(--ykt-border); border-radius: 6px; background: #fafafa; }\r\n\r\n/* ===== 题目列表面板 (#ykt-problem-list-panel) ===== */\r\n#ykt-problem-list{ display: flex; flex-direction: column; gap: 10px; }\r\n#ykt-problem-list .problem-row{\r\n  border: 1px solid var(--ykt-border); border-radius: 8px; padding: 8px; background: #fafafa;\r\n}\r\n#ykt-problem-list .problem-title{ font-weight: 600; margin-bottom: 4px; }\r\n#ykt-problem-list .problem-meta{ color: #666; font-size: 12px; margin-bottom: 6px; }\r\n#ykt-problem-list .problem-actions{ display: flex; gap: 8px; align-items: center; }\r\n#ykt-problem-list .problem-actions button{\r\n  height: 28px; padding: 0 10px; border-radius: 6px; border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer;\r\n}\r\n#ykt-problem-list .problem-actions button:hover{ background: #eef3ff; border-color: var(--ykt-accent); }\r\n#ykt-problem-list .problem-done{ color: #0a7a2f; font-weight: 600; }\r\n\r\n/* ===== 活动题目列表(右下角小卡片) ===== */\r\n#ykt-active-problems-panel.ykt-active-wrapper{\r\n  position: fixed; right: 20px; bottom: 60px; z-index: var(--ykt-z);\r\n}\r\n#ykt-active-problems{ display: flex; flex-direction: column; gap: 8px; max-height: 60vh; overflow: auto; }\r\n#ykt-active-problems .active-problem-card{\r\n  width: 320px; background: #fff; border: 1px solid var(--ykt-border);\r\n  border-radius: 8px; box-shadow: var(--ykt-shadow); padding: 10px;\r\n}\r\n#ykt-active-problems .ap-title{ font-weight: 600; margin-bottom: 4px; }\r\n#ykt-active-problems .ap-info{ color: #666; font-size: 12px; margin-bottom: 8px; }\r\n#ykt-active-problems .ap-actions{ display: flex; gap: 8px; }\r\n#ykt-active-problems .ap-actions button{\r\n  height: 28px; padding: 0 10px; border-radius: 6px; border: 1px solid var(--ykt-border-strong); background: #f7f8fa; cursor: pointer;\r\n}\r\n#ykt-active-problems .ap-actions button:hover{ background: #eef3ff; border-color: var(--ykt-accent); }\r\n\r\n/* ===== 教程面板 (#ykt-tutorial-panel) ===== */\r\n#ykt-tutorial-panel .tutorial-content h4{ margin: 8px 0 6px; }\r\n#ykt-tutorial-panel .tutorial-content p,\r\n#ykt-tutorial-panel .tutorial-content li{ line-height: 1.5; }\r\n#ykt-tutorial-panel .tutorial-content a{ color: var(--ykt-accent); text-decoration: none; }\r\n#ykt-tutorial-panel .tutorial-content a:hover{ text-decoration: underline; }\r\n\r\n/* ===== 小屏适配 ===== */\r\n@media (max-width: 1200px){\r\n  #ykt-presentation-panel{ width: 760px; }\r\n  #ykt-presentation-panel .panel-body{ grid-template-columns: 260px 1fr; }\r\n}\r\n@media (max-width: 900px){\r\n  .ykt-panel{ right: 12px; left: 12px; width: auto; }\r\n  #ykt-presentation-panel{ width: auto; }\r\n  #ykt-presentation-panel .panel-body{ grid-template-columns: 1fr; }\r\n}\r\n\r\n/* ===== 自动作答成功弹窗 ===== */\r\n.auto-answer-popup{\r\n  position: fixed; inset: 0; z-index: calc(var(--ykt-z) + 2);\r\n  background: rgba(0,0,0,.2);\r\n  display: flex; align-items: flex-end; justify-content: flex-end;\r\n  opacity: 0; transition: opacity .18s ease;\r\n}\r\n.auto-answer-popup.visible{ opacity: 1; }\r\n\r\n.auto-answer-popup .popup-content{\r\n  width: min(560px, 96vw);\r\n  background: #fff; border: 1px solid var(--ykt-border-strong);\r\n  border-radius: 10px; box-shadow: var(--ykt-shadow);\r\n  margin: 16px; overflow: hidden;\r\n}\r\n\r\n.auto-answer-popup .popup-header{\r\n  display: flex; align-items: center; justify-content: space-between;\r\n  gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--ykt-border);\r\n}\r\n.auto-answer-popup .popup-header h4{ margin: 0; font-size: 16px; }\r\n.auto-answer-popup .close-btn{ cursor: pointer; color: var(--ykt-muted); }\r\n.auto-answer-popup .close-btn:hover{ color: var(--ykt-hover); }\r\n\r\n.auto-answer-popup .popup-body{ padding: 10px 12px; display: flex; flex-direction: column; gap: 10px; }\r\n.auto-answer-popup .popup-row{ display: grid; grid-template-columns: 56px 1fr; gap: 8px; align-items: start; }\r\n.auto-answer-popup .label{ color: #666; font-size: 12px; line-height: 1.8; }\r\n.auto-answer-popup .content{ white-space: normal; word-break: break-word; }\r\n\r\n/* ===== 1.16.6: 课件浏览面板:固定右侧详细视图,左侧独立滚动 ===== */\r\n#ykt-presentation-panel {\r\n  --ykt-panel-max-h: 72vh;           /* 与 .ykt-panel 的最大高度保持一致 */\r\n}\r\n\r\n/* 两列布局:左列表 + 右详细视图 */\r\n#ykt-presentation-panel .panel-body{\r\n  display: grid;\r\n  grid-template-columns: 300px 1fr;  /* 左列宽度可按需调整 */\r\n  gap: 12px;\r\n  overflow: hidden;                  /* 避免内部再出现双滚动条 */\r\n  align-items: start;\r\n}\r\n\r\n/* 左侧:只让左列滚动,限制在面板可视高度内 */\r\n#ykt-presentation-panel .panel-left{\r\n  max-height: var(--ykt-panel-max-h);\r\n  overflow: auto;\r\n  min-width: 0;                      /* 防止子元素撑破 */\r\n}\r\n\r\n/* 右侧:粘性定位为“固定”,始终在面板可视区内 */\r\n#ykt-presentation-panel .panel-right{\r\n  position: sticky;\r\n  top: 0;                            /* 相对可滚动祖先(面板)吸顶 */\r\n  align-self: start;\r\n}\r\n\r\n/* 右侧详细视图自身也限制高度并允许内部滚动 */\r\n#ykt-presentation-panel .slide-view{\r\n  max-height: var(--ykt-panel-max-h);\r\n  overflow: auto;\r\n  border: 1px solid var(--ykt-border);\r\n  border-radius: 8px;\r\n  background: #fff;\r\n}\r\n\r\n/* 小屏自适配:堆叠布局时取消 sticky,避免遮挡 */\r\n@media (max-width: 900px){\r\n  #ykt-presentation-panel .panel-body{\r\n    grid-template-columns: 1fr;\r\n  }\r\n  #ykt-presentation-panel .panel-right{\r\n    position: static;\r\n  }\r\n}\r\n\r\n/* 在现有样式基础上添加 */\r\n\r\n.text-status {\r\n  font-size: 12px;\r\n  padding: 4px 8px;\r\n  border-radius: 4px;\r\n  margin: 4px 0;\r\n  display: inline-block;\r\n}\r\n\r\n.text-status.success {\r\n  background-color: #d4edda;\r\n  color: #155724;\r\n  border: 1px solid #c3e6cb;\r\n}\r\n\r\n.text-status.warning {\r\n  background-color: #fff3cd;\r\n  color: #856404;\r\n  border: 1px solid #ffeaa7;\r\n}\r\n\r\n.ykt-question-display {\r\n  background: #f8f9fa;\r\n  border: 1px solid #dee2e6;\r\n  border-radius: 4px;\r\n  padding: 8px;\r\n  margin: 4px 0;\r\n  max-height: 150px;\r\n  overflow-y: auto;\r\n  font-family: monospace;\r\n  font-size: 13px;\r\n  line-height: 1.4;\r\n}\r\n\r\n/* 在现有样式基础上添加 */\r\n\r\n.ykt-custom-prompt {\r\n  width: 100%;\r\n  min-height: 60px;\r\n  padding: 8px;\r\n  border: 1px solid #ddd;\r\n  border-radius: 4px;\r\n  font-family: inherit;\r\n  font-size: 13px;\r\n  line-height: 1.4;\r\n  resize: vertical;\r\n  background-color: #fff;\r\n  transition: border-color 0.3s ease;\r\n}\r\n\r\n.ykt-custom-prompt:focus {\r\n  outline: none;\r\n  border-color: #007bff;\r\n  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);\r\n}\r\n\r\n.ykt-custom-prompt::placeholder {\r\n  color: #999;\r\n  font-style: italic;\r\n}\r\n\r\n.ykt-custom-prompt:empty::before {\r\n  content: attr(placeholder);\r\n  color: #999;\r\n  font-style: italic;\r\n  pointer-events: none;\r\n}\r\n\r\n/* 确保输入框在暗色主题下也能正常显示 */\r\n.ykt-panel.dark .ykt-custom-prompt {\r\n  background-color: #2d3748;\r\n  border-color: #4a5568;\r\n  color: #e2e8f0;\r\n}\r\n\r\n.ykt-panel.dark .ykt-custom-prompt::placeholder {\r\n  color: #a0aec0;\r\n}\r\n\r\n.ykt-panel.dark .ykt-custom-prompt:focus {\r\n  border-color: #63b3ed;\r\n  box-shadow: 0 0 0 2px rgba(99, 179, 237, 0.25);\r\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();
  })();
})();