Greasy Fork

Greasy Fork is available in English.

轻小说文库下载 (优化版)

wenku8_dl_ug is a userscript for downloading Wenku8 novels. wenku8 下载器, 支持批量下载、EPUB格式转换、简繁体转换等功能。

目前为 2025-05-20 提交的版本,查看 最新版本

// ==UserScript==
// @name         轻小说文库下载 (优化版)
// @namespace    wenku8_dl_ug
// @version      2.3.1
// @author       HaoaW (Original) raventu (Refactor)
// @description  wenku8_dl_ug is a userscript for downloading Wenku8 novels. wenku8 下载器, 支持批量下载、EPUB格式转换、简繁体转换等功能。
// @icon         https://www.wenku8.net/favicon.ico
// @source       https://github.com/Raven-tu/wenku8_dl_ug
// @match        *://www.wenku8.net/*
// @match        *://www.wenku8.cc/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/full.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jszip.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
// @connect      wenku8.com
// @connect      wenku8.cc
// @connect      app.wenku8.com
// @connect      dl.wenku8.com
// @connect      img.wenku8.com
// @grant        GM_info
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// ==/UserScript==

(function (fileSaver, JSZip$1) {
  'use strict';

  var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();
  var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
  const OpenCCConverter = {
    COOKIE_KEY: "OpenCCwenku8",
    TARGET_ENCODING_COOKIE_KEY: "targetEncodingCookie",
    // 假设页面脚本定义了这个key
    COOKIE_DAYS: 7,
    buttonElement: null,
    isConversionEnabled: false,
    originalSimplized: null,
    originalTraditionalized: null,
    /**
     * 初始化 OpenCC 转换功能
     * @param {object} pageGlobals - 包含页面全局变量的对象 (如 unsafeWindow)
     */
    init(pageGlobals) {
      this.originalSimplized = pageGlobals.Simplized;
      this.originalTraditionalized = pageGlobals.Traditionalized;
      if (typeof pageGlobals.OpenCC === "undefined") {
        console.warn("OpenCC库未加载,简繁转换功能可能受限。");
        return;
      }
      this.isConversionEnabled = this.getCookie(this.COOKIE_KEY, pageGlobals) === "1";
      this.buttonElement = document.createElement("a");
      this.buttonElement.href = "javascript:void(0);";
      this.buttonElement.addEventListener("click", () => this.toggleConversion(pageGlobals));
      this.updateButtonText();
      if (this.isConversionEnabled) {
        if (typeof pageGlobals.Traditionalized !== "undefined") {
          pageGlobals.Traditionalized = pageGlobals.OpenCC.Converter({ from: "cn", to: "tw" });
        }
        if (typeof pageGlobals.Simplized !== "undefined") {
          pageGlobals.Simplized = pageGlobals.OpenCC.Converter({ from: "tw", to: "cn" });
        }
        if (this.getCookie(this.TARGET_ENCODING_COOKIE_KEY, pageGlobals) === "2" && typeof pageGlobals.translateBody === "function") {
          pageGlobals.targetEncoding = "2";
          pageGlobals.translateBody();
        }
      }
      const translateButton = document.querySelector(`#${pageGlobals.translateButtonId}`);
      if (translateButton && translateButton.parentElement) {
        translateButton.parentElement.appendChild(document.createTextNode("  "));
        translateButton.parentElement.appendChild(this.buttonElement);
      } else {
        console.warn("未能找到页面简繁转换按钮的挂载点。OpenCC切换按钮可能无法显示。");
      }
    },
    toggleConversion(pageGlobals) {
      if (this.isConversionEnabled) {
        this.setCookie(this.COOKIE_KEY, "", this.COOKIE_DAYS, pageGlobals);
      } else {
        this.setCookie(this.TARGET_ENCODING_COOKIE_KEY, "2", this.COOKIE_DAYS, pageGlobals);
        this.setCookie(this.COOKIE_KEY, "1", this.COOKIE_DAYS, pageGlobals);
      }
      location.reload();
    },
    updateButtonText() {
      this.buttonElement.innerHTML = this.isConversionEnabled ? "关闭(OpenCC)" : "开启(OpenCC)";
    },
    // 保持与旧代码一致的 setCookie 和 getCookie (可能由页面提供)
    setCookie(name2, value, days, pageGlobals) {
      if (typeof pageGlobals.setCookie === "function") {
        pageGlobals.setCookie(name2, value, days);
      } else {
        console.warn("pageGlobals.setCookie 未定义,OpenCC cookie 可能无法设置");
        let expires = "";
        if (days) {
          const date = /* @__PURE__ */ new Date();
          date.setTime(date.getTime() + days * 24 * 60 * 60 * 1e3);
          expires = `; expires=${date.toUTCString()}`;
        }
        document.cookie = `${name2}=${value || ""}${expires}; path=/`;
      }
    },
    getCookie(name2, pageGlobals) {
      if (typeof pageGlobals.getCookie === "function") {
        return pageGlobals.getCookie(name2);
      }
      console.warn("pageGlobals.getCookie 未定义,OpenCC cookie 可能无法读取");
      const nameEQ = `${name2}=`;
      const ca = document.cookie.split(";");
      for (let i = 0; i < ca.length; i++) {
        let c = ca[i];
        while (c.charAt(0) === " ") c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) === 0)
          return c.substring(nameEQ.length, c.length);
      }
      return null;
    }
  };
  const UILogger = {
    _progressElements: null,
    // DOM elements for progress display
    _showButton: null,
    // Button to show the progress bar after closing
    init() {
      if (this._progressElements)
        return;
      this._progressElements = {
        text: document.createElement("span"),
        image: document.createElement("span"),
        // Not directly used in main display but useful to hold counts
        error: document.createElement("div"),
        // Use a div to hold log entries
        main: document.createElement("div"),
        // Main container div
        controls: document.createElement("div")
        // Container for control buttons
      };
      this._progressElements.main.id = "epubDownloaderProgress";
      this._progressElements.main.style.cssText = "position: fixed; bottom: 0; left: 0; width: 100%; background-color: #f0f0f0; border-top: 1px solid #ccc; padding: 5px; z-index: 9999; font-size: 12px; color: #333; font-family: sans-serif;";
      this._progressElements.error.style.cssText = "max-height: 100px; overflow-y: auto; margin-top: 5px; border-top: 1px dashed #ccc; padding-top: 5px;";
      this._progressElements.controls.style.cssText = "position: absolute;right: 15px;top: 5px;display: flex;justify-content: flex-end;gap: 5px;margin-bottom: 5px;";
      const closeButton = document.createElement("button");
      closeButton.textContent = "-";
      closeButton.id = "closeProgress";
      this._progressElements.controls.appendChild(closeButton);
      this._showButton = document.createElement("button");
      this._showButton.textContent = "+";
      this._showButton.style.cssText = "position: fixed; bottom: 10px; right: 10px; z-index: 9999; display: none;";
      this._showButton.id = "showProgressButton";
      this._progressElements.main.appendChild(this._progressElements.controls);
      const titleElement = document.getElementById("title");
      if (titleElement && titleElement.parentElement) {
        titleElement.parentElement.insertBefore(this._progressElements.main, titleElement.nextSibling);
      } else {
        document.body.appendChild(this._progressElements.main);
        console.warn("[UILogger] Could not find #title element for progress bar insertion.");
      }
      document.body.appendChild(this._showButton);
      closeButton.addEventListener("click", () => this.closeProgress());
      this._showButton.addEventListener("click", () => this.showProgress());
      this.clearLog();
      this.updateProgress({ Text: [], Images: [], totalTasksAdded: 0, tasksCompletedOrSkipped: 0 }, "ePub下载器就绪...");
    },
    _ensureInitialized() {
      if (!this._progressElements || !document.getElementById("epubDownloaderProgress")) {
        this.init();
      }
    },
    closeProgress() {
      this._progressElements.main.style.display = "none";
      this._showButton.style.display = "block";
    },
    showProgress() {
      this._progressElements.main.style.display = "block";
      this._showButton.style.display = "none";
      document.getElementById("toggleProgress").textContent = "隐藏";
    },
    // updateProgress needs access to the bookInfo instance for counts
    updateProgress(bookInfoInstance, message) {
      this._ensureInitialized();
      if (message) {
        const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
        const logEntry = document.createElement("div");
        logEntry.className = "epub-log-entry";
        logEntry.innerHTML = `[${time}] ${message}`;
        this._progressElements.error.insertBefore(logEntry, this._progressElements.error.firstChild);
        while (this._progressElements.error.children.length > 300) {
          this._progressElements.error.removeChild(this._progressElements.error.lastChild);
        }
      }
      const textDownloaded = bookInfoInstance.Text.filter((t) => t.content).length;
      const totalTexts = bookInfoInstance.Text.length;
      const imagesDownloaded = bookInfoInstance.Images.filter((img) => img.content).length;
      const totalImages = bookInfoInstance.Images.length;
      const totalTasks = bookInfoInstance.totalTasksAdded;
      const completedTasks = bookInfoInstance.tasksCompletedOrSkipped;
      const progressHtml = `
            ePub生成进度:
            文本 ${textDownloaded}/${totalTexts};
            图片 ${imagesDownloaded}/${totalImages};
            任务 ${completedTasks}/${totalTasks};
            <br>最新日志:
        `;
      this._progressElements.main.innerHTML = progressHtml;
      this._progressElements.main.appendChild(this._progressElements.controls);
      if (this._progressElements.error.firstChild) {
        const latestLogClone = this._progressElements.error.firstChild.cloneNode(true);
        latestLogClone.style.display = "inline";
        latestLogClone.style.fontWeight = "bold";
        this._progressElements.main.appendChild(latestLogClone);
      } else {
        this._progressElements.main.appendChild(document.createTextNode("无"));
      }
      if (!this._progressElements.main.contains(this._progressElements.error)) {
        this._progressElements.main.appendChild(this._progressElements.error);
      }
    },
    logError(message) {
      this._ensureInitialized();
      console.error(`[UILogger] ${message}`);
      this.updateProgress(this.getMinimalBookInfo(), `<span style="color:red;">错误: ${message}</span>`);
    },
    logWarn(message) {
      this._ensureInitialized();
      console.warn(`[UILogger] ${message}`);
      this.updateProgress(this.getMinimalBookInfo(), `<span style="color:orange;">警告: ${message}</span>`);
    },
    logInfo(message) {
      this._ensureInitialized();
      console.log(`[UILogger] ${message}`);
      this.updateProgress(this.getMinimalBookInfo(), `<span style="color:green;">${message}</span>`);
    },
    clearLog() {
      this._ensureInitialized();
      this._progressElements.error.innerHTML = "";
      this._progressElements.main.innerHTML = "ePub生成进度: 文本 0/0;图片 0/0;任务 0/0;<br>最新日志: 无";
      this._progressElements.main.appendChild(this._progressElements.controls);
      if (!this._progressElements.main.contains(this._progressElements.error)) {
        this._progressElements.main.appendChild(this._progressElements.error);
      }
    },
    getMinimalBookInfo() {
      return this._bookInfoInstance || {
        Text: [],
        Images: [],
        totalTasksAdded: 0,
        tasksCompletedOrSkipped: 0
      };
    }
  };
  const name = "wenku8_dl_ug";
  const version = "2.3.1";
  const author = "HaoaW (Original) raventu (Refactor)";
  const repository = { "url": "https://github.com/Raven-tu/wenku8_dl_ug" };
  const Package = {
    name,
    version,
    author,
    repository
  };
  const CURRENT_URL = new URL(_unsafeWindow.location.href);
  const EPUB_EDITOR_CONFIG_UID = "24A08AE1-E132-458C-9E1D-6C998F16A666";
  const IMG_LOCATION_FILENAME = "ImgLocation";
  const XML_ILLEGAL_CHARACTERS_REGEX = /[\x00-\x08\v\f\x0E-\x1F]/g;
  const APP_API_DOMAIN = "app.wenku8.com";
  const APP_API_PATH = "/android.php";
  const DOWNLOAD_DOMAIN = "dl.wenku8.com";
  const IMAGE_DOMAIN = "img.wenku8.com";
  const MAX_XHR_RETRIES = 3;
  const XHR_TIMEOUT_MS = 2e4;
  const XHR_RETRY_DELAY_MS = 500;
  const VOLUME_ID_PREFIX = "Volume";
  const IMAGE_FILE_PREFIX = "Img";
  const TEXT_SPAN_PREFIX = "Txt";
  const PROJECT_NAME = Package.name;
  const PROJECT_AUTHOR = Package.author;
  const PROJECT_VERSION = Package.version;
  const PROJECT_REPO = Package.repository.url;
  const XHRDownloadManager = {
    _queue: [],
    _activeDownloads: 0,
    _maxConcurrentDownloads: 4,
    // 并发下载数控制
    _bookInfoInstance: null,
    // 关联的EpubBuilder实例
    hasCriticalFailure: false,
    // 标记是否有关键下载失败
    init(bookInfoInstance) {
      this._bookInfoInstance = bookInfoInstance;
      this._queue = [];
      this._activeDownloads = 0;
      this.hasCriticalFailure = false;
    },
    /**
     * 添加一个下载任务到队列
     * @param {object} xhrTask - 任务对象 {url, loadFun, data?, type?, isCritical?}
     *   - url: 请求URL (可选,对于非URL任务如appChapterList)
     *   - loadFun: 实际执行下载的异步函数 (接收 xhrTask 作为参数)
     *   - data: 任务相关数据
     *   - type: 任务类型 (用于日志和判断关键性)
     *   - isCritical: 是否为关键任务 (默认为true,图片等非关键任务可设为false)
     */
    add(xhrTask) {
      if (this.hasCriticalFailure) {
        this._bookInfoInstance.logger.logWarn(`关键下载已失败,新任务 ${xhrTask.type || xhrTask.url} 被跳过。`);
        this._bookInfoInstance.totalTasksAdded++;
        this._bookInfoInstance.tasksCompletedOrSkipped++;
        this._bookInfoInstance.tryBuildEpub();
        return;
      }
      xhrTask.XHRRetryCount = 0;
      xhrTask.isCritical = xhrTask.isCritical !== void 0 ? xhrTask.isCritical : true;
      this._queue.push(xhrTask);
      this._bookInfoInstance.totalTasksAdded++;
      this._processQueue();
    },
    _processQueue() {
      if (this.hasCriticalFailure)
        return;
      while (this._activeDownloads < this._maxConcurrentDownloads && this._queue.length > 0) {
        const task = this._queue.shift();
        this._activeDownloads++;
        task.loadFun(task).then(() => {
        }).catch((err) => {
          this._bookInfoInstance.logger.logError(`任务 ${task.type || task.url} 执行时发生意外错误: ${err}`);
          task.done = true;
          this.taskFinished(task, task.isCritical);
        });
      }
    },
    /**
     * 任务完成(成功或最终失败)时调用
     * @param {object} task - 完成的任务对象
     * @param {boolean} [isFinalFailure] - 任务是否最终失败
     */
    taskFinished(task, isFinalFailure = false) {
      if (task._finished)
        return;
      task._finished = true;
      this._activeDownloads--;
      this._bookInfoInstance.tasksCompletedOrSkipped++;
      if (isFinalFailure && task.isCritical && !this.hasCriticalFailure) {
        this.hasCriticalFailure = true;
        this._bookInfoInstance.XHRFail = true;
        this._bookInfoInstance.logger.logError("一个关键下载任务最终失败,后续部分任务可能被取消。");
        this._queue = [];
      }
      this._processQueue();
      this._bookInfoInstance.tryBuildEpub();
    },
    /**
     * 任务需要重试时调用
     * @param {object} xhrTask - 需要重试的任务对象
     * @param {string} message - 重试原因消息
     */
    retryTask(xhrTask, message) {
      if (this.hasCriticalFailure) {
        this._bookInfoInstance.logger.logWarn(`重试 ${xhrTask.type || xhrTask.url} 被跳过,因为关键下载已失败。`);
        xhrTask.done = true;
        this.taskFinished(xhrTask, true);
        return;
      }
      xhrTask.XHRRetryCount = (xhrTask.XHRRetryCount || 0) + 1;
      if (xhrTask.XHRRetryCount <= MAX_XHR_RETRIES) {
        this._bookInfoInstance.refreshProgress(this._bookInfoInstance, `${message} (尝试次数 ${xhrTask.XHRRetryCount}/${MAX_XHR_RETRIES})`);
        this._activeDownloads--;
        this._queue.unshift(xhrTask);
        setTimeout(() => this._processQueue(), XHR_RETRY_DELAY_MS * xhrTask.XHRRetryCount);
      } else {
        this._bookInfoInstance.refreshProgress(this._bookInfoInstance, `<span style="color:red;">${xhrTask.type || xhrTask.url} 超出最大重试次数, 下载失败!</span>`);
        xhrTask.done = true;
        this.taskFinished(xhrTask, xhrTask.isCritical);
      }
    },
    /**
     * 检查所有任务是否完成 (包括队列中和正在进行的)
     * @returns {boolean}
     */
    areAllTasksDone() {
      return this._bookInfoInstance.tasksCompletedOrSkipped >= this._bookInfoInstance.totalTasksAdded && this._queue.length === 0 && this._activeDownloads === 0;
    }
  };
  function gmXmlHttpRequestAsync(details) {
    return new Promise((resolve, reject) => {
      _GM_xmlhttpRequest({
        ...details,
        onload: resolve,
        onerror: (err) => {
          console.error(`GM_xmlhttpRequest error for ${details.url}:`, err);
          reject(err);
        },
        ontimeout: (err) => {
          console.error(`GM_xmlhttpRequest timeout for ${details.url}:`, err);
          reject(err);
        }
      });
    });
  }
  async function fetchAsText(url, encoding = "gbk") {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status} for ${url}`);
    }
    const buffer = await response.arrayBuffer();
    const decoder = new TextDecoder(encoding);
    return decoder.decode(buffer);
  }
  function cleanXmlIllegalChars(text) {
    return text.replace(XML_ILLEGAL_CHARACTERS_REGEX, "");
  }
  const AppApiService = {
    volumeChapterData: /* @__PURE__ */ new Map(),
    // 存储卷的章节列表 { vid: [{cid, cName, content}]}
    chapterListXml: null,
    // 缓存书籍的章节列表XML Document
    isChapterListLoading: false,
    chapterListWaitQueue: [],
    // 等待章节列表的请求xhr包装对象
    disableTraditionalChineseRequest: true,
    // 默认禁用APP接口请求繁体,由前端OpenCC处理
    _getApiLanguageParam(bookInfo) {
      const targetEncoding = (bookInfo == null ? void 0 : bookInfo.targetEncoding) || _unsafeWindow.targetEncoding;
      if (this.disableTraditionalChineseRequest || !targetEncoding) {
        return "0";
      }
      return targetEncoding === "1" ? "1" : "0";
    },
    _encryptRequestBody(body) {
      return `appver=1.0&timetoken=${Number(/* @__PURE__ */ new Date())}&request=${btoa(body)}`;
    },
    /**
     * 从App接口获取书籍章节列表XML
     * @param {EpubBuilderCoordinator} bookInfo - 协调器实例
     * @returns {Promise<void>}
     */
    async _fetchChapterList(bookInfo) {
      if (this.isChapterListLoading)
        return;
      this.isChapterListLoading = true;
      bookInfo.refreshProgress(bookInfo, `下载App章节目录...`);
      const langParam = this._getApiLanguageParam(bookInfo);
      const requestBody = this._encryptRequestBody(`action=book&do=list&aid=${bookInfo.aid}&t=${langParam}`);
      const url = `http://${APP_API_DOMAIN}${APP_API_PATH}`;
      try {
        const response = await gmXmlHttpRequestAsync({
          method: "POST",
          url,
          headers: { "content-type": "application/x-www-form-urlencoded;charset=utf-8" },
          data: requestBody,
          timeout: XHR_TIMEOUT_MS
        });
        if (response.status === 200) {
          const parser = new DOMParser();
          this.chapterListXml = parser.parseFromString(cleanXmlIllegalChars(response.responseText), "application/xml");
          bookInfo.refreshProgress(bookInfo, `App章节目录下载完成。`);
          this.chapterListWaitQueue.forEach((queuedXhr) => this.loadVolumeChapters(queuedXhr));
          this.chapterListWaitQueue = [];
        } else {
          throw new Error(`Status ${response.status}`);
        }
      } catch (error) {
        bookInfo.logger.logError(`App章节目录下载失败: ${error.message}`);
        bookInfo.XHRManager.taskFinished({ type: "appChapterList", isCritical: true }, true);
      } finally {
        this.isChapterListLoading = false;
      }
    },
    /**
     * 加载指定分卷的章节内容 (从App接口)
     * @param {object} xhrVolumeRequest - 卷的XHR任务对象 (由VolumeLoader或Coordinator创建)
     *   - bookInfo: 协调器实例
     *   - data: { vid, vcssText, Text }
     *   - dealVolume: 处理函数 (通常是 VolumeLoader.dealVolumeText)
     */
    async loadVolumeChapters(xhrVolumeRequest) {
      const { bookInfo, data: volumeData } = xhrVolumeRequest;
      if (!this.chapterListXml) {
        this.chapterListWaitQueue.push(xhrVolumeRequest);
        if (!this.isChapterListLoading) {
          this._fetchChapterList(bookInfo);
        }
        return;
      }
      const volumeElement = Array.from(this.chapterListXml.getElementsByTagName("volume")).find((vol) => vol.getAttribute("vid") === volumeData.vid);
      if (!volumeElement) {
        bookInfo.refreshProgress(bookInfo, `<span style="color:fuchsia;">App章节目录未找到分卷 ${volumeData.vid},无法生成ePub。</span>`);
        bookInfo.XHRManager.taskFinished(xhrVolumeRequest, true);
        return;
      }
      const chapters = Array.from(volumeElement.children).map((ch) => ({
        cid: ch.getAttribute("cid"),
        cName: ch.textContent,
        content: null
        // 稍后填充
      }));
      this.volumeChapterData.set(volumeData.vid, chapters);
      chapters.forEach((chapter) => {
        const chapterXhr = {
          type: "appChapter",
          // 自定义类型
          url: `http://${APP_API_DOMAIN}${APP_API_PATH}`,
          loadFun: (xhr) => this._fetchChapterContent(xhr),
          // 指向内部方法
          dealVolume: xhrVolumeRequest.dealVolume,
          // 传递处理函数
          data: { ...volumeData, cid: chapter.cid, cName: chapter.cName, isAppApi: true },
          bookInfo,
          isCritical: true
          // 章节内容是关键任务
        };
        bookInfo.XHRManager.add(chapterXhr);
      });
      bookInfo.XHRManager.taskFinished(xhrVolumeRequest, false);
    },
    /**
     * 从App接口获取单个章节内容
     * @param {object} xhrChapterRequest - 章节的XHR任务对象
     *   - bookInfo: 协调器实例
     *   - data: { vid, cid, cName, isAppApi, Text }
     *   - dealVolume: 处理函数 (VolumeLoader.dealVolumeText)
     */
    async _fetchChapterContent(xhrChapterRequest) {
      const { bookInfo, data } = xhrChapterRequest;
      const langParam = this._getApiLanguageParam(bookInfo);
      const requestBody = this._encryptRequestBody(`action=book&do=text&aid=${bookInfo.aid}&cid=${data.cid}&t=${langParam}`);
      const failureMessage = `${data.cName} 下载失败`;
      try {
        const response = await gmXmlHttpRequestAsync({
          method: "POST",
          url: xhrChapterRequest.url,
          headers: { "content-type": "application/x-www-form-urlencoded;charset=utf-8" },
          data: requestBody,
          timeout: XHR_TIMEOUT_MS
        });
        if (response.status === 200) {
          const chapterVolumeData = this.volumeChapterData.get(data.vid);
          const chapterEntry = chapterVolumeData.find((c) => c.cid === data.cid);
          if (chapterEntry) {
            chapterEntry.content = response.responseText;
          }
          bookInfo.refreshProgress(bookInfo, `${data.cName} 下载完成。`);
          if (chapterVolumeData.every((c) => c.content !== null)) {
            let combinedVolumeText = "";
            for (const chap of chapterVolumeData) {
              if (!chap.content)
                continue;
              let content = chap.content;
              content = content.replace(chap.cName, `<div class="chaptertitle"><a name="${chap.cid}">${chap.cName}</a></div><div class="chaptercontent">`);
              content = content.replace(/\r\n/g, "<br />\r\n");
              if (content.includes("<!--image-->http")) {
                content = content.replace(/<!--image-->(http[\w:/.?@#&=%]+)<!--image-->/g, (match, p1) => `<div class="divimage" title="${p1}"></div>`);
              }
              content += `</div>`;
              combinedVolumeText += content;
            }
            const pseudoVolumeXhr = {
              bookInfo,
              VolumeIndex: bookInfo.Text.findIndex((t) => t.vid === data.vid),
              // 找到对应的卷索引
              data: { ...data, Text: bookInfo.Text.find((t) => t.vid === data.vid) }
              // 传递卷的Text对象
            };
            xhrChapterRequest.dealVolume(pseudoVolumeXhr, combinedVolumeText);
          }
          bookInfo.XHRManager.taskFinished(xhrChapterRequest, false);
        } else {
          throw new Error(`Status ${response.status}`);
        }
      } catch (error) {
        bookInfo.logger.logError(`${failureMessage} 错误: ${error.message}`);
        bookInfo.XHRManager.retryTask(xhrChapterRequest, failureMessage);
      }
    },
    /**
     * 对外暴露的接口,用于从App接口加载章节内容(通常用于版权受限页面)
     * @param {object} bookInfo - 包含 aid, logger, refreshProgress 的对象
     * @param {string} chapterId - 章节ID
     * @param {HTMLElement} contentElement - 显示内容的DOM元素
     * @param {Function} translateBodyFunc - 页面提供的翻译函数
     */
    async fetchChapterForReading(bookInfo, chapterId, contentElement, translateBodyFunc) {
      const langParam = this._getApiLanguageParam({ targetEncoding: _unsafeWindow.targetEncoding });
      const requestBody = this._encryptRequestBody(`action=book&do=text&aid=${bookInfo.aid}&cid=${chapterId}&t=${langParam}`);
      const url = `http://${APP_API_DOMAIN}${APP_API_PATH}`;
      contentElement.innerHTML = "正在通过App接口加载内容,请稍候...";
      try {
        const response = await gmXmlHttpRequestAsync({
          method: "POST",
          url,
          headers: { "content-type": "application/x-www-form-urlencoded;charset=utf-8" },
          data: requestBody,
          timeout: XHR_TIMEOUT_MS
        });
        if (response.status === 200) {
          let rawText = response.responseText;
          rawText = rawText.replace(/ {2}\S.*/, "");
          rawText = rawText.replace(/\r\n/g, "<br />\r\n");
          if (rawText.includes("<!--image-->http")) {
            rawText = rawText.replace(/<!--image-->(http[\w:/.?@#&=%]+)<!--image-->/g, (m, p1) => `<div class="divimage"><a href="${p1}" target="_blank"><img src="${p1}" border="0" class="imagecontent"></a></div>`);
          }
          contentElement.innerHTML = rawText;
          if (typeof translateBodyFunc === "function") {
            translateBodyFunc(contentElement);
          }
        } else {
          contentElement.innerHTML = `通过App接口加载内容失败,状态码: ${response.status}`;
        }
      } catch (error) {
        contentElement.innerHTML = `通过App接口加载内容失败: ${error.message}`;
        console.error("App接口内容加载失败:", error);
      }
    }
  };
  const VolumeLoader = {
    /**
     * 加载网页版分卷文本内容
     * @param {object} xhr - XHR任务对象
     *   - bookInfo: 协调器实例
     *   - url: 下载URL
     *   - VolumeIndex: 卷在 bookInfo.nav_toc 和 bookInfo.Text 中的索引
     *   - data: { vid, vcssText, Text }
     */
    async loadWebVolumeText(xhr) {
      const { bookInfo, url, VolumeIndex, data } = xhr;
      const volumeInfo = bookInfo.nav_toc[VolumeIndex];
      const failureMessage = `${volumeInfo.volumeName} 下载失败`;
      try {
        const response = await gmXmlHttpRequestAsync({ method: "GET", url, timeout: XHR_TIMEOUT_MS });
        if (response.status === 200) {
          bookInfo.refreshProgress(bookInfo, `${volumeInfo.volumeName} 网页版内容下载完成。`);
          this.dealVolumeText(xhr, response.responseText);
          bookInfo.XHRManager.taskFinished(xhr, false);
        } else if (response.status === 404) {
          bookInfo.refreshProgress(bookInfo, `${volumeInfo.volumeName} 网页版404,尝试使用App接口下载...`);
          xhr.dealVolume = this.dealVolumeText.bind(this);
          AppApiService.loadVolumeChapters(xhr);
          bookInfo.XHRManager.taskFinished(xhr, false);
        } else {
          throw new Error(`Status ${response.status}`);
        }
      } catch (error) {
        bookInfo.logger.logError(`${failureMessage} 错误: ${error.message}`);
        bookInfo.XHRManager.retryTask(xhr, failureMessage);
      }
    },
    /**
     * 加载书籍内容简介
     * @param {object} xhr - XHR任务对象
     *   - bookInfo: 协调器实例
     *   - url: 下载URL
     */
    async loadBookDescription(xhr) {
      const { bookInfo, url } = xhr;
      const failureMessage = `内容简介下载失败`;
      try {
        const text = await fetchAsText(url, "gbk");
        const parser = new DOMParser();
        const doc = parser.parseFromString(text, "text/html");
        const descSpan = doc.evaluate("//span[@class='hottext' and contains(text(),'内容简介:')]/following-sibling::span", doc, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;
        if (descSpan) {
          bookInfo.description = descSpan.textContent.trim();
        }
        bookInfo.refreshProgress(bookInfo, `内容简介下载完成。`);
        bookInfo.XHRManager.taskFinished(xhr, false);
      } catch (error) {
        bookInfo.logger.logError(`${failureMessage} 错误: ${error.message}`);
        bookInfo.XHRManager.retryTask(xhr, failureMessage);
      }
    },
    /**
     * 加载图片资源
     * @param {object} xhr - XHR任务对象
     *   - bookInfo: 协调器实例
     *   - url: 图片URL
     *   - images: 图片条目信息 {path, content, id, idName, TextId, coverImgChk?, smallCover?}
     */
    async loadImage(xhr) {
      const { bookInfo, url, images: imageInfo } = xhr;
      const failureMessage = `${imageInfo.idName} 下载失败`;
      try {
        const response = await gmXmlHttpRequestAsync({ method: "GET", url, responseType: "arraybuffer", timeout: XHR_TIMEOUT_MS });
        if (response.status === 200) {
          imageInfo.content = response.response;
          if (imageInfo.coverImgChk && !bookInfo.Images.some((i) => i.coverImg)) {
            try {
              imageInfo.Blob = new Blob([imageInfo.content], { type: "image/jpeg" });
              imageInfo.ObjectURL = URL.createObjectURL(imageInfo.Blob);
              const img = new Image();
              img.onload = () => {
                imageInfo.coverImg = img.naturalHeight / img.naturalWidth > 1.2;
                URL.revokeObjectURL(imageInfo.ObjectURL);
                delete imageInfo.Blob;
                delete imageInfo.ObjectURL;
                bookInfo.refreshProgress(bookInfo, `${imageInfo.idName} 下载完成${imageInfo.coverImg ? " (设为封面候选)" : ""}。`);
                bookInfo.XHRManager.taskFinished(xhr, false);
              };
              img.onerror = () => {
                URL.revokeObjectURL(imageInfo.ObjectURL);
                delete imageInfo.Blob;
                delete imageInfo.ObjectURL;
                bookInfo.logger.logError(`${imageInfo.idName} 图片对象加载失败。`);
                bookInfo.XHRManager.taskFinished(xhr, false);
              };
              img.src = imageInfo.ObjectURL;
            } catch (e) {
              bookInfo.logger.logError(`${imageInfo.idName} 创建Blob/ObjectURL失败: ${e.message}`);
              bookInfo.XHRManager.taskFinished(xhr, false);
            }
          } else {
            bookInfo.refreshProgress(bookInfo, `${imageInfo.idName} 下载完成。`);
            bookInfo.XHRManager.taskFinished(xhr, false);
          }
        } else {
          throw new Error(`Status ${response.status}`);
        }
      } catch (error) {
        bookInfo.logger.logError(`${failureMessage} 错误: ${error.message}`);
        bookInfo.XHRManager.retryTask(xhr, failureMessage);
      }
    },
    /**
     * 处理下载到的分卷文本内容 (网页版或App版合并后的)
     * @param {object} xhr - 原始的卷XHR任务对象 (或AppService伪造的对象)
     *   - bookInfo: 协调器实例
     *   - VolumeIndex: 卷在 bookInfo.nav_toc 和 bookInfo.Text 中的索引
     *   - data: { vid, vcssText, Text }
     * @param {string} htmlText - 下载到的HTML或合并后的章节文本
     */
    dealVolumeText(xhr, htmlText) {
      const { bookInfo, VolumeIndex, data } = xhr;
      const volumeTextData = data.Text;
      const navTocEntry = volumeTextData.navToc;
      let chapterCounter = 0;
      let imageCounter = 0;
      let textNodeCounter = 0;
      const parser = new DOMParser();
      const tempDoc = parser.parseFromString(
        `<html><head><meta charset="utf-8"/></head><body></body></html>`,
        "text/html"
      );
      tempDoc.body.innerHTML = htmlText;
      if (typeof _unsafeWindow.currentEncoding !== "undefined" && typeof _unsafeWindow.targetEncoding !== "undefined" && _unsafeWindow.currentEncoding !== _unsafeWindow.targetEncoding && typeof _unsafeWindow.translateBody === "function") {
        _unsafeWindow.translateBody(tempDoc.body);
      }
      const elementsToRemove = [];
      Array.from(tempDoc.body.children).forEach((child) => {
        if (child.tagName === "UL" && child.id === "contentdp") {
          elementsToRemove.push(child);
        } else if (child.tagName === "DIV" && child.className === "chaptertitle") {
          chapterCounter++;
          const chapterTitleText = child.textContent.trim();
          const chapterDivId = `chapter_${chapterCounter}`;
          child.innerHTML = `<div id="${chapterDivId}"><h3>${cleanXmlIllegalChars(chapterTitleText)}</h3></div>`;
          if (navTocEntry) {
            navTocEntry.chapterArr.push({
              chapterName: chapterTitleText,
              chapterID: chapterDivId,
              chapterHref: `${navTocEntry.volumeHref}#${chapterDivId}`
            });
          }
          const titleSpan = tempDoc.createElement("span");
          titleSpan.id = `${TEXT_SPAN_PREFIX}_${chapterDivId}`;
          titleSpan.className = "txtDropEnable";
          titleSpan.setAttribute("ondragover", "return false");
          child.parentElement.insertBefore(titleSpan, child);
          titleSpan.appendChild(child);
        } else if (child.tagName === "DIV" && child.className === "chaptercontent") {
          Array.from(child.childNodes).forEach((contentNode) => {
            if (contentNode.nodeType === Node.TEXT_NODE && contentNode.textContent.trim() !== "") {
              textNodeCounter++;
              const textSpan = tempDoc.createElement("span");
              textSpan.id = `${TEXT_SPAN_PREFIX}_${VolumeIndex}_${textNodeCounter}`;
              textSpan.className = "txtDropEnable";
              textSpan.setAttribute("ondragover", "return false");
              child.insertBefore(textSpan, contentNode);
              textSpan.appendChild(contentNode);
            } else if (contentNode.tagName === "DIV" && contentNode.className === "divimage" && contentNode.hasAttribute("title")) {
              const imgSrc = contentNode.getAttribute("title");
              if (imgSrc) {
                const imgUrl = new URL(imgSrc);
                const imgFileName = imgUrl.pathname.split("/").pop();
                const imgPathInEpub = `Images${imgUrl.pathname}`;
                contentNode.innerHTML = `<img src="../${imgPathInEpub}" alt="${cleanXmlIllegalChars(imgFileName)}"/>`;
                imageCounter++;
                const imageId = `${IMAGE_FILE_PREFIX}_${VolumeIndex}_${imageCounter}`;
                const imageEntry = {
                  path: imgPathInEpub,
                  content: null,
                  // 稍后下载
                  id: imageId,
                  idName: imgFileName,
                  // 文件名作为标识
                  TextId: volumeTextData.id,
                  // 关联到分卷ID
                  coverImgChk: VolumeIndex === 0 && imageCounter <= 2
                  // 前两张图作为封面候选
                };
                if (!bookInfo.Images.some((img) => img.path === imageEntry.path)) {
                  bookInfo.Images.push(imageEntry);
                  bookInfo.XHRManager.add({
                    type: "image",
                    url: imgSrc,
                    loadFun: (imgXhr) => VolumeLoader.loadImage(imgXhr),
                    // 静态方法调用
                    images: imageEntry,
                    // 传递图片条目信息
                    bookInfo,
                    isCritical: false
                    // 图片不是关键任务
                  });
                }
              }
            }
          });
        }
      });
      elementsToRemove.forEach((el) => el.parentElement.removeChild(el));
      volumeTextData.content = tempDoc.body.innerHTML;
      if (VolumeIndex === 0 && !bookInfo.thumbnailImageAdded) {
        const pathParts = CURRENT_URL.pathname.replace("novel", "image").split("/");
        pathParts.pop();
        const bookNumericId = pathParts.find((p) => /^\d+$/.test(p));
        if (bookNumericId) {
          const thumbnailImageId = `${bookNumericId}s`;
          const thumbnailSrc = `https://${IMAGE_DOMAIN}${pathParts.join("/")}/${thumbnailImageId}.jpg`;
          const thumbnailPathInEpub = `Images/${bookNumericId}/${thumbnailImageId}.jpg`;
          const thumbnailEntry = {
            path: thumbnailPathInEpub,
            content: null,
            id: thumbnailImageId,
            // 使用图片本身的ID
            idName: `${thumbnailImageId}.jpg`,
            TextId: "",
            // 不关联特定卷,作为通用封面候选
            smallCover: true,
            // 标记为缩略图封面候选
            isCritical: false
            // 缩略图不是关键任务
          };
          if (!bookInfo.Images.some((img) => img.id === thumbnailEntry.id)) {
            bookInfo.Images.push(thumbnailEntry);
            bookInfo.XHRManager.add({
              type: "image",
              url: thumbnailSrc,
              loadFun: (thumbXhr) => VolumeLoader.loadImage(thumbXhr),
              images: thumbnailEntry,
              bookInfo,
              isCritical: false
            });
            bookInfo.thumbnailImageAdded = true;
          }
        }
      }
      if (!bookInfo.descriptionXhrInitiated) {
        bookInfo.descriptionXhrInitiated = true;
        bookInfo.XHRManager.add({
          type: "description",
          url: `/book/${bookInfo.aid}.htm`,
          // 相对路径
          loadFun: (descXhr) => VolumeLoader.loadBookDescription(descXhr),
          bookInfo,
          isCritical: true
          // 简介是关键任务
        });
      }
      bookInfo.tryBuildEpub();
    }
  };
  const EpubEditor = {
    novelTableElement: null,
    config: {
      UID: EPUB_EDITOR_CONFIG_UID,
      aid: _unsafeWindow.article_id,
      // 来自页面全局变量
      pathname: CURRENT_URL.pathname,
      ImgLocation: []
    },
    imgLocationRegex: [/img/i, /插图/, /插圖/, /\.jpg/i, /\.png/i],
    styleLinks: [],
    editorRootElement: null,
    lastClickedVolumeLI: null,
    _bookInfoInstance: null,
    // 保存EpubBuilder实例引用
    _currentVolumeImageMap: /* @__PURE__ */ new Map(),
    // 当前卷可用的图片映射 {idName: imgElement}
    /**
     * 初始化ePub编辑器
     * @param {EpubBuilderCoordinator} bookInfoInstance - 协调器实例
     */
    init(bookInfoInstance) {
      this._bookInfoInstance = bookInfoInstance;
      document.querySelectorAll(".DownloadAll").forEach((el) => el.style.pointerEvents = "none");
      this.novelTableElement = document.body.getElementsByTagName("table")[0];
      if (this.novelTableElement)
        this.novelTableElement.style.display = "none";
      const editorCss = document.createElement("link");
      editorCss.type = "text/css";
      editorCss.rel = "stylesheet";
      editorCss.href = "/themes/wenku8/style.css";
      document.head.appendChild(editorCss);
      this.styleLinks.push(editorCss);
      this.editorRootElement = document.createElement("div");
      this.editorRootElement.id = "ePubEidter";
      this.editorRootElement.style.display = "none";
      editorCss.onload = () => {
        this.editorRootElement.style.display = "";
      };
      this.editorRootElement.innerHTML = this._getEditorHtmlTemplate();
      if (this.novelTableElement && this.novelTableElement.parentElement) {
        this.novelTableElement.parentElement.insertBefore(this.editorRootElement, this.novelTableElement);
      } else {
        document.body.appendChild(this.editorRootElement);
        console.warn("未能找到合适的编辑器挂载点,编辑器已追加到body。");
      }
      document.getElementById("EidterBuildBtn").addEventListener("click", (event) => this.handleBuildEpubClick(event));
      document.getElementById("EidterImportBtn").addEventListener("click", (event) => this.handleImportConfigClick(event));
      document.getElementById("VolumeImg").addEventListener("drop", (event) => this.handleImageDeleteDrop(event));
      document.getElementById("VolumeImg").addEventListener("dragover", (event) => event.preventDefault());
      this.config.ImgLocation = bookInfoInstance.ImgLocation;
      document.getElementById("CfgArea").value = JSON.stringify(this.config, null, "  ");
      this._populateVolumeList();
    },
    /**
     * 销毁编辑器DOM和事件监听器
     */
    destroy() {
      if (this.editorRootElement && this.editorRootElement.parentElement) {
        this.editorRootElement.parentElement.removeChild(this.editorRootElement);
      }
      this.styleLinks.forEach((link) => link.parentElement && link.parentElement.removeChild(link));
      if (this.novelTableElement)
        this.novelTableElement.style.display = "";
      document.querySelectorAll(".DownloadAll").forEach((el) => el.style.pointerEvents = "auto");
      this.editorRootElement = null;
      this._bookInfoInstance = null;
      this._currentVolumeImageMap.clear();
    },
    /**
     * 填充分卷列表
     */
    _populateVolumeList() {
      const volumeUl = document.getElementById("VolumeUL");
      if (!volumeUl)
        return;
      volumeUl.innerHTML = "";
      let firstLi = null;
      this._bookInfoInstance.Text.forEach((textEntry) => {
        const li = document.createElement("li");
        const a = document.createElement("a");
        a.href = "javascript:void(0);";
        a.id = textEntry.id;
        a.textContent = textEntry.volumeName;
        li.appendChild(a);
        li.addEventListener("click", (event) => this.handleVolumeClick(event, textEntry));
        volumeUl.appendChild(li);
        if (!firstLi)
          firstLi = li;
      });
      if (firstLi)
        firstLi.click();
    },
    /**
     * 处理分卷列表点击事件
     * @param {MouseEvent} event
     * @param {object} textEntry - 当前卷的文本数据 {path, content, id, vid, volumeName, navToc}
     */
    handleVolumeClick(event, textEntry) {
      if (this.lastClickedVolumeLI) {
        this.lastClickedVolumeLI.firstElementChild.style.color = "";
      }
      this.lastClickedVolumeLI = event.currentTarget;
      this.lastClickedVolumeLI.firstElementChild.style.color = "fuchsia";
      const volumeTextDiv = document.getElementById("VolumeText");
      if (!volumeTextDiv)
        return;
      volumeTextDiv.style.display = "none";
      volumeTextDiv.innerHTML = textEntry.content;
      this._populateImageListForVolume(textEntry);
      this._populateGuessedImageLocations(textEntry);
      this._populateChapterNavForVolume(textEntry);
      volumeTextDiv.style.display = "";
      volumeTextDiv.scrollTop = 0;
      const volumeImgDiv = document.getElementById("VolumeImg");
      if (volumeImgDiv)
        volumeImgDiv.scrollTop = 0;
    },
    /**
     * 填充当前卷可用的图片列表
     * @param {object} textEntry - 当前卷的文本数据
     */
    _populateImageListForVolume(textEntry) {
      const volumeImgDiv = document.getElementById("VolumeImg");
      if (!volumeImgDiv)
        return;
      volumeImgDiv.innerHTML = "";
      this._currentVolumeImageMap.clear();
      this._bookInfoInstance.Images.filter((img) => img.TextId === textEntry.id || img.smallCover).forEach((imageInfo) => {
        if (!imageInfo.ObjectURL && imageInfo.content) {
          try {
            imageInfo.Blob = new Blob([imageInfo.content], { type: "image/jpeg" });
            imageInfo.ObjectURL = URL.createObjectURL(imageInfo.Blob);
          } catch (e) {
            console.error(`创建图片Blob失败 for ${imageInfo.idName}:`, e);
            return;
          }
        } else if (!imageInfo.ObjectURL && !imageInfo.content) {
          console.warn(`图片 ${imageInfo.idName} 既无ObjectURL也无content,无法显示。`);
          return;
        }
        const div = document.createElement("div");
        div.className = "editor-image-item";
        const img = document.createElement("img");
        img.setAttribute("imgID", imageInfo.idName);
        img.src = imageInfo.ObjectURL;
        img.height = 127;
        img.draggable = true;
        img.addEventListener("dragstart", (event) => this.handleImageDragStart(event, imageInfo));
        this._currentVolumeImageMap.set(imageInfo.idName, img);
        div.appendChild(img);
        div.appendChild(document.createElement("br"));
        div.appendChild(document.createTextNode(imageInfo.id));
        volumeImgDiv.appendChild(div);
      });
    },
    /**
     * 填充推测的插图位置列表并在文本中标记
     * @param {object} textEntry - 当前卷的文本数据
     */
    _populateGuessedImageLocations(textEntry) {
      const imgUl = document.getElementById("ImgUL");
      if (!imgUl)
        return;
      imgUl.innerHTML = "";
      const currentVolumeLocations = this._bookInfoInstance.ImgLocation.filter((loc) => loc.vid === textEntry.vid);
      document.querySelectorAll("#VolumeText .txtDropEnable").forEach((dropTargetSpan) => {
        dropTargetSpan.addEventListener("drop", (event) => this.handleTextDrop(event, textEntry));
        dropTargetSpan.addEventListener("dragover", (event) => event.preventDefault());
        currentVolumeLocations.filter((loc) => loc.spanID === dropTargetSpan.id).forEach((loc) => {
          const draggedImgElement = this._currentVolumeImageMap.get(loc.imgID);
          if (draggedImgElement) {
            const divImage = document.createElement("div");
            divImage.className = "divimageM";
            divImage.innerHTML = draggedImgElement.outerHTML;
            const actualImgInDiv = divImage.firstElementChild;
            actualImgInDiv.id = `${loc.spanID}_${loc.imgID}`;
            actualImgInDiv.draggable = true;
            actualImgInDiv.addEventListener("dragstart", (ev) => this.handlePlacedImageDragStart(ev, loc));
            dropTargetSpan.parentNode.insertBefore(divImage, dropTargetSpan);
          }
        });
        if (!dropTargetSpan.firstElementChild || dropTargetSpan.firstElementChild.className !== "chaptertitle") {
          for (const regex of this.imgLocationRegex) {
            if (regex.test(dropTargetSpan.textContent)) {
              const li = document.createElement("li");
              const a = document.createElement("a");
              a.href = "javascript:void(0);";
              a.setAttribute("SpanID", dropTargetSpan.id);
              a.textContent = `${dropTargetSpan.textContent.replace(/\s/g, "").substring(0, 12)}...`;
              li.appendChild(a);
              li.addEventListener("click", () => this.handleGuessedLocationClick(dropTargetSpan));
              imgUl.appendChild(li);
              dropTargetSpan.style.color = "fuchsia";
              break;
            }
          }
        }
      });
    },
    /**
     * 填充章节导航列表
     * @param {object} textEntry - 当前卷的文本数据
     */
    _populateChapterNavForVolume(textEntry) {
      const chapterUl = document.getElementById("ChapterUL");
      if (!chapterUl)
        return;
      chapterUl.innerHTML = "";
      const tocEntry = this._bookInfoInstance.nav_toc.find((toc) => toc.volumeID === textEntry.id);
      if (tocEntry && tocEntry.chapterArr) {
        tocEntry.chapterArr.forEach((chapter) => {
          const li = document.createElement("li");
          const a = document.createElement("a");
          a.href = "javascript:void(0);";
          a.setAttribute("chapterID", chapter.chapterID);
          a.textContent = chapter.chapterName;
          li.appendChild(a);
          li.addEventListener("click", () => this.handleChapterNavClick(chapter.chapterID));
          chapterUl.appendChild(li);
        });
      }
    },
    /**
     * 处理从图片列表拖动图片开始事件
     * @param {DragEvent} event
     * @param {object} imageInfo - 图片条目信息
     */
    handleImageDragStart(event, imageInfo) {
      event.dataTransfer.setData("text/plain", imageInfo.idName);
      event.dataTransfer.setData("sourceType", "newImage");
    },
    /**
     * 处理从文本中拖动已放置图片开始事件
     * @param {DragEvent} event
     * @param {object} imgLocation - 图片位置配置 {vid, spanID, imgID}
     */
    handlePlacedImageDragStart(event, imgLocation) {
      event.dataTransfer.setData("text/plain", JSON.stringify(imgLocation));
      event.dataTransfer.setData("sourceType", "placedImage");
      event.dataTransfer.setData("elementId", event.target.id);
    },
    /**
     * 处理拖动图片到文本区域的放置事件
     * @param {DragEvent} event
     * @param {object} textEntry - 当前卷的文本数据
     */
    handleTextDrop(event, textEntry) {
      event.preventDefault();
      const sourceType = event.dataTransfer.getData("sourceType");
      if (sourceType === "newImage") {
        const imgIdName = event.dataTransfer.getData("text/plain");
        const dropTargetSpan = event.currentTarget;
        const newLocation = {
          vid: textEntry.vid,
          spanID: dropTargetSpan.id,
          imgID: imgIdName
        };
        if (this._bookInfoInstance.ImgLocation.some((loc) => loc.vid === newLocation.vid && loc.spanID === newLocation.spanID && loc.imgID === newLocation.imgID)) {
          this._bookInfoInstance.logger.logWarn(`尝试在 ${newLocation.spanID} 放置重复图片 ${newLocation.imgID}。`);
          return;
        }
        this._bookInfoInstance.ImgLocation.push(newLocation);
        const draggedImgElement = this._currentVolumeImageMap.get(imgIdName);
        if (draggedImgElement) {
          const divImage = document.createElement("div");
          divImage.className = "divimageM";
          divImage.innerHTML = draggedImgElement.outerHTML;
          const actualImgInDiv = divImage.firstElementChild;
          actualImgInDiv.id = `${newLocation.spanID}_${newLocation.imgID}`;
          actualImgInDiv.draggable = true;
          actualImgInDiv.addEventListener("dragstart", (ev) => this.handlePlacedImageDragStart(ev, newLocation));
          dropTargetSpan.parentNode.insertBefore(divImage, dropTargetSpan);
        }
        this._updateConfigTextarea();
      }
    },
    /**
     * 处理拖动已放置图片到删除区域的放置事件
     * @param {DragEvent} event
     */
    handleImageDeleteDrop(event) {
      event.preventDefault();
      const sourceType = event.dataTransfer.getData("sourceType");
      if (sourceType === "placedImage") {
        const imgLocationJson = event.dataTransfer.getData("text/plain");
        const elementIdToRemove = event.dataTransfer.getData("elementId");
        try {
          const locToRemove = JSON.parse(imgLocationJson);
          this._bookInfoInstance.ImgLocation = this._bookInfoInstance.ImgLocation.filter(
            (loc) => !(loc.vid === locToRemove.vid && loc.spanID === locToRemove.spanID && loc.imgID === locToRemove.imgID)
          );
          const domElementToRemove = document.getElementById(elementIdToRemove);
          if (domElementToRemove && domElementToRemove.parentElement.className === "divimageM") {
            domElementToRemove.parentElement.remove();
          }
          this._updateConfigTextarea();
          this._bookInfoInstance.logger.logInfo(`已移除图片 ${locToRemove.imgID} 在 ${locToRemove.spanID} 的位置配置。`);
        } catch (e) {
          console.error("解析拖放数据失败:", e);
          this._bookInfoInstance.logger.logError("删除图片配置失败。");
        }
      }
    },
    /**
     * 处理推测插图位置列表点击事件 (滚动到文本位置)
     * @param {HTMLElement} dropTargetSpan - 对应的文本span元素
     */
    handleGuessedLocationClick(dropTargetSpan) {
      const volumeTextDiv = document.getElementById("VolumeText");
      if (volumeTextDiv && dropTargetSpan) {
        volumeTextDiv.scroll({ top: dropTargetSpan.offsetTop - 130, behavior: "smooth" });
      }
    },
    /**
     * 处理章节导航列表点击事件 (滚动到章节位置)
     * @param {string} chapterDomId - 章节对应的DOM ID
     */
    handleChapterNavClick(chapterDomId) {
      const targetElement = document.getElementById(chapterDomId);
      if (targetElement) {
        const volumeTextDiv = document.getElementById("VolumeText");
        if (volumeTextDiv) {
          volumeTextDiv.scroll({ top: targetElement.offsetTop, behavior: "smooth" });
        }
      }
    },
    /**
     * 处理“生成ePub”按钮点击事件
     * @param {MouseEvent} event
     */
    handleBuildEpubClick(event) {
      var _a, _b;
      event.currentTarget.disabled = true;
      this._bookInfoInstance.ePubEidtDone = true;
      this._bookInfoInstance.tryBuildEpub();
      if (((_a = document.getElementById("SendArticle")) == null ? void 0 : _a.checked) && this._bookInfoInstance.ImgLocation.length > 0) {
        this._sendConfigToServer();
      }
      if ((_b = document.getElementById("ePubEditerClose")) == null ? void 0 : _b.checked) {
        setTimeout(() => {
          if (this._bookInfoInstance && this._bookInfoInstance.XHRManager.areAllTasksDone()) {
            this.destroy();
          } else {
            this._bookInfoInstance.logger.logInfo("ePub生成未完成或失败,编辑器未自动关闭。");
          }
        }, 3e3);
      }
    },
    /**
     * 处理“导入配置”按钮点击事件
     * @param {MouseEvent} event
     */
    handleImportConfigClick(event) {
      event.currentTarget.disabled = true;
      const cfgArea = document.getElementById("CfgArea");
      if (!cfgArea) {
        event.currentTarget.disabled = false;
        return;
      }
      try {
        const importedCfg = JSON.parse(cfgArea.value);
        if (importedCfg && importedCfg.UID === this.config.UID && importedCfg.aid === this.config.aid && Array.isArray(importedCfg.ImgLocation) && importedCfg.ImgLocation.length > 0) {
          let importedCount = 0;
          importedCfg.ImgLocation.forEach((iCfg) => {
            if (!this._bookInfoInstance.Text.some((t) => t.vid === iCfg.vid)) {
              console.warn(`导入配置跳过无效卷ID: ${iCfg.vid}`);
              return;
            }
            if (!iCfg.spanID || !iCfg.imgID) {
              console.warn(`导入配置跳过无效项: ${JSON.stringify(iCfg)}`);
              return;
            }
            if (!this._bookInfoInstance.ImgLocation.some(
              (loc) => loc.vid === iCfg.vid && loc.spanID === iCfg.spanID && loc.imgID === iCfg.imgID
            )) {
              this._bookInfoInstance.ImgLocation.push({
                vid: String(iCfg.vid),
                // 确保vid是字符串,与内部存储一致
                spanID: String(iCfg.spanID),
                imgID: String(iCfg.imgID)
              });
              importedCount++;
            }
          });
          this._updateConfigTextarea();
          this._bookInfoInstance.logger.logInfo(`成功导入 ${importedCount} 条插图位置配置。`);
          if (this.lastClickedVolumeLI)
            this.lastClickedVolumeLI.click();
        } else {
          console.error("导入的配置格式不正确或与当前书籍不匹配。");
        }
      } catch (e) {
        console.error("导入配置失败:JSON格式错误。");
        console.error("导入配置解析错误:", e);
      }
      event.currentTarget.disabled = false;
    },
    /**
     * 更新配置文本框内容
     */
    _updateConfigTextarea() {
      this.config.ImgLocation = this._bookInfoInstance.ImgLocation;
      const cfgArea = document.getElementById("CfgArea");
      if (cfgArea)
        cfgArea.value = JSON.stringify(this.config, null, "  ");
    },
    /**
     * 将配置发送到书评区
     */
    _sendConfigToServer() {
      const cfgToSend = { ...this.config };
      const imgLocJson = JSON.stringify(this._bookInfoInstance.ImgLocation);
      const zip = new JSZip();
      zip.file(IMG_LOCATION_FILENAME, imgLocJson, {
        compression: "DEFLATE",
        compressionOptions: { level: 9 }
      });
      zip.generateAsync({ type: "base64", mimeType: "application/zip" }).then((base64Data) => {
        cfgToSend.ImgLocationBase64 = base64Data;
        delete cfgToSend.ImgLocation;
        const uniqueVolumeNames = [...new Set(
          this._bookInfoInstance.ImgLocation.map((loc) => this._bookInfoInstance.nav_toc.find((toc) => toc.vid === loc.vid)).filter(Boolean).map((toc) => toc.volumeName)
        )];
        const postContent = `包含分卷列表:${uniqueVolumeNames.join(", ")}
[code]${JSON.stringify(cfgToSend)}[/code]`;
        const postData = /* @__PURE__ */ new Map([
          ["ptitle", "ePub插图位置 (优化版脚本)"],
          ["pcontent", postContent]
        ]);
        const postUrl = `/modules/article/reviews.php?aid=${this._bookInfoInstance.aid}`;
        const iframe = document.createElement("iframe");
        iframe.style.display = "none";
        document.body.appendChild(iframe);
        const iframeDoc = iframe.contentWindow.document;
        const form = iframeDoc.createElement("form");
        form.acceptCharset = "gbk";
        form.method = "POST";
        form.action = postUrl;
        postData.forEach((value, key) => {
          const input = iframeDoc.createElement("input");
          input.type = "hidden";
          input.name = key;
          input.value = value;
          form.appendChild(input);
        });
        iframeDoc.body.appendChild(form);
        form.submit();
        setTimeout(() => iframe.remove(), 5e3);
        this._bookInfoInstance.logger.logInfo("插图配置已尝试发送到书评区。");
      }).catch((err) => {
        this._bookInfoInstance.logger.logError(`压缩配置失败,无法发送到书评区: ${err.message}`);
        console.error("Zip config error:", err);
      });
    },
    /**
     * 获取编辑器HTML模板字符串
     * @returns {string} HTML模板
     */
    _getEditorHtmlTemplate() {
      return `
            <style>
                .editor-image-item { float: left; text-align: center; height: 155px; overflow: hidden; margin: 0 2px; border: 1px solid #ccc; padding: 2px; }
                .editor-image-item img { cursor: grab; }
                #VolumeImg[ondragover="return false"] { border: 2px dashed #ccc; padding: 5px; background-color: #f0f0f0; min-height: 160px;}
                .divimageM { border: 1px dotted blue; padding: 2px; margin: 2px 0; display: inline-block; }
                .divimageM img { display: block; max-width: 100%; }
                #ePubEidter .main { width: 1200px; margin: 0 auto; } /* 居中 */
                #ePubEidter #left { float: left; width: 200px; margin-right: 10px; }
                #ePubEidter #centerm { overflow: hidden; } /* 占据剩余空间 */
                #ePubEidter .block { border: 1px solid #ccc; margin-bottom: 10px; }
                #ePubEidter .blocktitle { background-color: #eee; padding: 5px; font-weight: bold; }
                #ePubEidter .blockcontent { padding: 5px; }
                #ePubEidter .ulrow { list-style: none; padding: 0; margin: 0; }
                #ePubEidter .ulrow li { margin-bottom: 5px; }
                #ePubEidter .ulrow a { text-decoration: none; color: #333; }
                #ePubEidter .ulrow a:hover { text-decoration: underline; }
                #ePubEidter .cb { clear: both; }
                #ePubEidter .textarea { width: 95%; }
                #ePubEidter .button { margin-right: 5px; }
                #ePubEidter .grid { border-collapse: collapse; width: 100%; }
                #ePubEidter .grid td { border: 1px solid #ccc; padding: 5px; vertical-align: top; }
                #ePubEidter .grid caption { font-weight: bold; margin-bottom: 5px; }
                #ePubEidter #VolumeText { height:500px; overflow: hidden scroll ;max-width: 900px; } /* 限制宽度 */
            </style>
            <div class="main">
                <!--左 章节-->
                <div id="left">
                    <div class="block" style="min-height: 230px;">
                        <div class="blocktitle"><span class="txt">操作设置</span><span class="txtr"></span></div>
                        <div class="blockcontent"><div style="padding-left:10px">
                            <ul class="ulrow">
                                <li><label for="SendArticle">将配置发送到书评:</label><input type="checkbox" id="SendArticle" /></li>
                                <li><label for="ePubEditerClose">生成后自动关闭:</label><input type="checkbox" id="ePubEditerClose" checked="true" /></li>
                                <li>配置内容:</li>
                                <li><textarea id="CfgArea" class="textarea" style="height:100px;"></textarea></li>
                                <li><input type="button" id="EidterImportBtn" class="button" value="导入配置" /></li>
                                <li><input type="button" id="EidterBuildBtn" class="button" value="生成ePub" /></li>
                            </ul><div class="cb"></div>
                        </div></div>
                    </div>
                    <div class="block" style="min-height: 230px;">
                        <div class="blocktitle"><span class="txt">分卷</span><span class="txtr"></span></div>
                        <div class="blockcontent"><div style="padding-left:10px">
                            <ul id="VolumeUL" class="ulrow"></ul><div class="cb"></div>
                        </div></div>
                    </div>
                </div>
                <!--左 章节-->
                <div id="left">
                    <div class="block" style="min-height: 230px;">
                        <div class="blocktitle"><span class="txt">推测插图位置</span><span class="txtr"></span></div>
                        <div class="blockcontent"><div style="padding-left:10px">
                            <ul id="ImgUL" class="ulrow" style="max-height: 200px; overflow-y: auto;"></ul><div class="cb"></div>
                        </div></div>
                    </div>
                    <div class="block" style="min-height: 230px;">
                        <div class="blocktitle"><span class="txt">章节</span><span class="txtr"></span></div>
                        <div class="blockcontent"><div style="padding-left:10px">
                            <ul id="ChapterUL" class="ulrow" style="max-height: 200px; overflow-y: auto;"></ul><div class="cb"></div>
                        </div></div>
                    </div>
                </div>
                <!--右 内容-->
                <div id="centerm">
                    <div id="content">
                        <table class="grid" width="100%" align="center"><tbody>
                            <tr>
                                <td width="4%" align="center"><span style="font-size:16px;">分<br>卷<br>插<br>图<br><br>(可拖拽图片到下方文本)<br><br>(将已放置图片拖到此处可删除)</span></td>
                                <td><div ondragover="return false" id="VolumeImg" style="height:155px;overflow:auto"></div></td>
                            </tr>
                        </tbody></table>
                        <table class="grid" width="100%" align="center">
                            <caption>分卷内容 (可拖入图片)</caption><tbody>
                                <tr><td><div id="VolumeText" style="height:500px;overflow: hidden scroll ;max-width: 900px;"></div></td></tr>
                            </tbody></table>
                    </div>
                </div>
            </div>`;
    }
  };
  const EpubFileBuilder = {
    MIMETYPE: "application/epub+zip",
    CONTAINER_XML: `<?xml version="1.0" ?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml" /></rootfiles></container>`,
    DEFAULT_CSS: {
      content: `nav#landmarks, nav#page-list { display:none; } ol { list-style-type: none; } .volumetitle, .chaptertitle { text-align: center; } .divimage { text-align: center; margin-top: 0.5em; margin-bottom: 0.5em; } .divimage img { max-width: 100%; height: auto; vertical-align: middle; }`,
      id: "default_css_id",
      path: "Styles/default.css"
    },
    NAV_XHTML_TEMPLATE: {
      content: `<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="zh-CN" xml:lang="zh-CN"><head><title>目录</title><meta charset="utf-8"/><link href="../Styles/default.css" rel="stylesheet" type="text/css"/></head><body epub:type="frontmatter"><nav epub:type="toc" id="toc" role="doc-toc"><h2><a href="#toc">目录</a></h2></nav></body></html>`,
      path: `Text/nav.xhtml`,
      id: `nav_xhtml_id`
    },
    /**
     * 构建并生成ePub文件
     * @param {EpubBuilderCoordinator} bookInfo - 协调器实例
     */
    async build(bookInfo) {
      var _a;
      if (bookInfo.XHRManager.hasCriticalFailure) {
        bookInfo.refreshProgress(bookInfo, `<span style="color:red;">关键文件下载失败,无法生成ePub。</span>`);
        const buildBtn = document.getElementById("EidterBuildBtn");
        if (buildBtn)
          buildBtn.disabled = false;
        return;
      }
      if (!bookInfo.XHRManager.areAllTasksDone()) {
        bookInfo.refreshProgress(bookInfo, `等待下载任务完成... (${bookInfo.tasksCompletedOrSkipped}/${bookInfo.totalTasksAdded})`);
        return;
      }
      if (bookInfo.ePubEidt && !bookInfo.ePubEidtDone) {
        bookInfo.refreshProgress(bookInfo, `等待用户编辑插图位置...`);
        if (!EpubEditor.editorRootElement) {
          EpubEditor.init(bookInfo);
        }
        return;
      }
      bookInfo.refreshProgress(bookInfo, `开始生成ePub文件...`);
      _unsafeWindow._isUnderConstruction = true;
      const zip = new JSZip$1();
      zip.file("mimetype", this.MIMETYPE, { compression: "STORE" });
      zip.file("META-INF/container.xml", this.CONTAINER_XML);
      const oebpsFolder = zip.folder("OEBPS");
      const contentOpfDoc = this._createContentOpfDocument(bookInfo);
      const manifest = contentOpfDoc.querySelector("manifest");
      const spine = contentOpfDoc.querySelector("spine");
      this._addManifestItem(manifest, this.DEFAULT_CSS.id, this.DEFAULT_CSS.path, "text/css");
      oebpsFolder.file(this.DEFAULT_CSS.path, this.DEFAULT_CSS.content);
      const navXhtmlContent = this._generateNavXhtml(bookInfo.nav_toc);
      const navItem = this._addManifestItem(manifest, this.NAV_XHTML_TEMPLATE.id, this.NAV_XHTML_TEMPLATE.path, "application/xhtml+xml");
      navItem.setAttribute("properties", "nav");
      this._addSpineItem(spine, this.NAV_XHTML_TEMPLATE.id, "no");
      oebpsFolder.file(this.NAV_XHTML_TEMPLATE.path, navXhtmlContent);
      bookInfo.Text.forEach((textEntry) => {
        this._addManifestItem(manifest, textEntry.id, textEntry.path, "application/xhtml+xml");
        this._addSpineItem(spine, textEntry.id);
        const finalHtml = this._processAndCleanVolumeHtml(textEntry, bookInfo);
        oebpsFolder.file(textEntry.path, finalHtml);
      });
      let coverImageId = null;
      const userCover = bookInfo.ImgLocation.find((loc) => loc.isCover);
      if (userCover) {
        const imgEntry = bookInfo.Images.find((img) => img.idName === userCover.imgID);
        if (imgEntry)
          coverImageId = imgEntry.id;
      }
      if (!coverImageId) {
        const coverImage = bookInfo.Images.find((img) => img.coverImg) || bookInfo.Images.find((img) => img.coverImgChk) || bookInfo.Images.find((img) => img.smallCover);
        if (coverImage)
          coverImageId = coverImage.id;
      }
      bookInfo.Images.forEach((imgEntry) => {
        if (imgEntry.content) {
          const item = this._addManifestItem(manifest, imgEntry.id, imgEntry.path, "image/jpeg");
          if (imgEntry.id === coverImageId) {
            item.setAttribute("properties", "cover-image");
          }
          oebpsFolder.file(imgEntry.path, imgEntry.content, { binary: true });
        } else {
          bookInfo.logger.logWarn(`图片 ${imgEntry.idName} 内容为空,未打包进ePub。`);
        }
      });
      if (bookInfo.ImgLocation && bookInfo.ImgLocation.length > 0) {
        const editorCfg = {
          UID: EPUB_EDITOR_CONFIG_UID,
          aid: bookInfo.aid,
          pathname: CURRENT_URL.pathname,
          ImgLocation: bookInfo.ImgLocation
        };
        const cfgJson = JSON.stringify(editorCfg, null, "  ");
        oebpsFolder.file("Other/ePubEditorCfg.json", cfgJson);
        this._addManifestItem(manifest, "editor_cfg_json", "Other/ePubEditorCfg.json", "application/json");
        bookInfo.logger.logInfo("编辑器配置已保存到ePub。");
      }
      this._populateMetadata(contentOpfDoc, bookInfo, coverImageId);
      const serializer = new XMLSerializer();
      let contentOpfString = serializer.serializeToString(contentOpfDoc);
      contentOpfString = contentOpfString.replace(/ xmlns=""/g, "");
      const content_opf_final = `${contentOpfString}`;
      oebpsFolder.file("content.opf", content_opf_final);
      let epubFileName = `${bookInfo.title}.${bookInfo.nav_toc[0].volumeName}`;
      if (bookInfo.nav_toc.length > 1) {
        epubFileName += `-${bookInfo.nav_toc[bookInfo.nav_toc.length - 1].volumeName}`;
      }
      epubFileName = epubFileName.replace(/[\\/:*?"<>|]/g, "_");
      try {
        const blob = await zip.generateAsync({ type: "blob", mimeType: "application/epub+zip" });
        fileSaver.saveAs(blob, `${epubFileName}.epub`);
        bookInfo.refreshProgress(bookInfo, `<span style="color:green;">ePub生成完成, 文件名:${epubFileName}.epub</span>`);
      } catch (err) {
        bookInfo.logger.logError(`ePub压缩或保存失败: ${err.message}`);
        bookInfo.refreshProgress(bookInfo, `<span style="color:red;">ePub生成失败: ${err.message}</span>`);
      } finally {
        const buildBtn = document.getElementById("EidterBuildBtn");
        if (buildBtn)
          buildBtn.disabled = false;
      }
      if (bookInfo.ePubEidtDone && EpubEditor.editorRootElement && ((_a = document.getElementById("ePubEditerClose")) == null ? void 0 : _a.checked)) {
        setTimeout(() => EpubEditor.destroy(), 1e3);
      }
      return true;
    },
    /**
     * 创建 content.opf 的 DOM 文档
     * @param {EpubBuilderCoordinator} bookInfo - 协调器实例
     * @returns {Document} OPF DOM 文档
     */
    _createContentOpfDocument(bookInfo) {
      const parser = new DOMParser();
      const opfString = `<?xml version="1.0" encoding="utf-8"?>
                <package version="3.0" unique-identifier="BookId" xmlns="http://www.idpf.org/2007/opf">
                    <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
                        <dc:language>zh-CN</dc:language>
                        <dc:title>${cleanXmlIllegalChars(bookInfo.title)}.${cleanXmlIllegalChars(bookInfo.nav_toc[0].volumeName)}</dc:title>
                        <dc:identifier id="BookId">urn:uuid:${globalThis.crypto.randomUUID()}</dc:identifier>
                        <dc:creator>${cleanXmlIllegalChars(bookInfo.creator || "未知作者")}</dc:creator>
                        <meta property="dcterms:modified">${`${(/* @__PURE__ */ new Date()).toISOString().split(".")[0]}Z`}</meta>
                        <meta builder="${PROJECT_NAME}" author="${PROJECT_AUTHOR}" version="${PROJECT_VERSION}">${PROJECT_REPO}</meta>
                        ${bookInfo.description ? `<dc:description>${cleanXmlIllegalChars(bookInfo.description)}</dc:description>` : ""}
                    </metadata>
                    <manifest></manifest>
                    <spine></spine>
                </package>`;
      const doc = parser.parseFromString(opfString, "text/xml");
      const parseError = doc.getElementsByTagName("parsererror");
      if (parseError.length > 0) {
        console.error("解析OPF XML时出错:", parseError[0].textContent);
        bookInfo.logger.logError("内部错误:创建OPF文档失败。");
        throw new Error("Failed to create OPF document due to parser error.");
      }
      return doc;
    },
    /**
     * 填充 metadata 元素
     * @param {Element} metadata - metadata DOM 元素
     * @param {EpubBuilderCoordinator} bookInfo - 协调器实例
     * @param {string} coverImageId - 封面图片的 manifest ID
     */
    _populateMetadata(metadata, bookInfo, coverImageId) {
      const _metadata = metadata.querySelector("metadata");
      if (coverImageId) {
        const coverMeta = metadata.createElement("meta");
        coverMeta.setAttribute("name", "cover");
        coverMeta.setAttribute("content", coverImageId);
        coverMeta.removeAttribute("xmlns");
        _metadata.appendChild(coverMeta);
      }
    },
    /**
     * 向 manifest 添加 item
     * @param {Element} manifestElement - manifest DOM 元素
     * @param {string} id - item ID
     * @param {string} href - item 路径
     * @param {string} mediaType - item MIME 类型
     * @returns {Element} 创建的 item 元素
     */
    _addManifestItem(manifestElement, id, href, mediaType) {
      const item = manifestElement.ownerDocument.createElement("item");
      item.setAttribute("id", id);
      item.setAttribute("href", href);
      item.setAttribute("media-type", mediaType);
      manifestElement.appendChild(item);
      return item;
    },
    /**
     * 向 spine 添加 itemref
     * @param {Element} spineElement - spine DOM 元素
     * @param {string} idref - 关联的 manifest item ID
     * @param {string} [linear] - 是否线性阅读
     * @returns {Element} 创建的 itemref 元素
     */
    _addSpineItem(spineElement, idref, linear = "yes") {
      const itemref = spineElement.ownerDocument.createElement("itemref");
      itemref.setAttribute("idref", idref);
      if (linear === "no") {
        itemref.setAttribute("linear", "no");
      }
      spineElement.appendChild(itemref);
      return itemref;
    },
    /**
     * 生成导航文件 (nav.xhtml) 的内容
     * @param {Array<object>} navTocEntries - 导航目录数据 (bookInfo.nav_toc)
     * @returns {string} nav.xhtml 内容字符串
     */
    _generateNavXhtml(navTocEntries) {
      const parser = new DOMParser();
      const doc = parser.parseFromString(cleanXmlIllegalChars(this.NAV_XHTML_TEMPLATE.content), "application/xhtml+xml");
      const tocNavElement = doc.getElementById("toc");
      const ol = doc.createElement("ol");
      navTocEntries.forEach((volumeToc) => {
        const vLi = doc.createElement("li");
        const vA = doc.createElement("a");
        vA.href = volumeToc.volumeHref;
        vA.textContent = volumeToc.volumeName;
        vLi.appendChild(vA);
        if (volumeToc.chapterArr && volumeToc.chapterArr.length > 0) {
          const cOl = doc.createElement("ol");
          volumeToc.chapterArr.forEach((chapterToc) => {
            const cLi = doc.createElement("li");
            const cA = doc.createElement("a");
            cA.href = chapterToc.chapterHref;
            cA.textContent = chapterToc.chapterName;
            cLi.appendChild(cA);
            cOl.appendChild(cLi);
          });
          vLi.appendChild(cOl);
        }
        ol.appendChild(vLi);
      });
      tocNavElement.appendChild(ol);
      const serializer = new XMLSerializer();
      let xhtmlString = serializer.serializeToString(doc.documentElement);
      xhtmlString = xhtmlString.replace(/ xmlns=""/g, "");
      return `<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
${xhtmlString}`;
    },
    /**
     * 处理并清理分卷HTML内容,插入图片等
     * @param {object} textEntry - 当前卷的文本数据 {path, content, id, vid, ...}
     * @param {EpubBuilderCoordinator} bookInfo - 协调器实例
     * @returns {string} 处理后的 XHTML 字符串
     */
    _processAndCleanVolumeHtml(textEntry, bookInfo) {
      const parser = new DOMParser();
      const initialHtml = `<?xml version="1.0" encoding="utf-8"?>
                <!DOCTYPE html>
                <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="zh-CN" xml:lang="zh-CN">
                <head>
                    <meta charset="utf-8"/>
                    <title>${cleanXmlIllegalChars(textEntry.volumeName)}</title>
                    <link href="../Styles/default.css" rel="stylesheet" type="text/css"/>
                </head>
                <body>
                   <div class="volumetitle"><h2>${cleanXmlIllegalChars(textEntry.volumeName)}</h2></div><br />
                   ${textEntry.content}
                </body>
                </html>`;
      const doc = parser.parseFromString(cleanXmlIllegalChars(initialHtml), "text/html");
      const body = doc.body;
      const volumeSpecificLocations = bookInfo.ImgLocation.filter((loc) => loc.vid === textEntry.vid);
      const volumeImages = bookInfo.Images.filter((img) => img.TextId === textEntry.id || img.smallCover);
      Array.from(body.querySelectorAll(".txtDropEnable")).forEach((span) => {
        const spanId = span.id;
        const childNodes = Array.from(span.childNodes);
        volumeSpecificLocations.filter((loc) => loc.spanID === spanId).forEach((loc) => {
          const imgEntry = volumeImages.find((img) => img.idName === loc.imgID && img.content);
          if (imgEntry) {
            const divImage = doc.createElement("div");
            divImage.setAttribute("class", "divimage");
            const imgTag = doc.createElement("img");
            imgTag.setAttribute("src", `../${imgEntry.path}`);
            imgTag.setAttribute("alt", cleanXmlIllegalChars(imgEntry.idName));
            imgTag.setAttribute("loading", "lazy");
            divImage.appendChild(imgTag);
            span.parentNode.insertBefore(divImage, span);
          } else {
            bookInfo.logger.logWarn(`配置的图片 ${loc.imgID} 在卷 ${textEntry.volumeName} (span: ${loc.spanID}) 未找到或未下载,未插入。`);
          }
        });
        childNodes.forEach((node) => {
          span.parentNode.insertBefore(node, span);
        });
        span.parentNode.removeChild(span);
      });
      Array.from(body.getElementsByTagName("p")).forEach((p) => {
        if (p.innerHTML.trim() === "" || p.innerHTML.trim() === "&nbsp;") {
          p.parentNode.removeChild(p);
        }
      });
      Array.from(body.getElementsByTagName("img")).forEach((img) => {
        if (!img.closest("div.divimage")) {
          const wrapper = doc.createElement("div");
          wrapper.className = "divimage";
          img.parentNode.insertBefore(wrapper, img);
          wrapper.appendChild(img);
        }
      });
      const serializer = new XMLSerializer();
      let xhtmlString = serializer.serializeToString(doc.documentElement);
      xhtmlString = xhtmlString.replace(/ xmlns=""/g, "");
      return `<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html>
${xhtmlString}`;
    }
  };
  class EpubBuilderCoordinator {
    /**
     * @param {boolean} isEditingMode - 是否进入编辑模式
     * @param {boolean} downloadAllVolumes - 是否下载全部分卷
     */
    constructor(isEditingMode = false, downloadAllVolumes = false) {
      var _a, _b, _c, _d, _e;
      this.aid = _unsafeWindow.article_id;
      this.title = ((_c = (_b = (_a = document.getElementById("title")) == null ? void 0 : _a.childNodes[0]) == null ? void 0 : _b.textContent) == null ? void 0 : _c.trim()) || "未知标题";
      this.creator = ((_e = (_d = document.getElementById("info")) == null ? void 0 : _d.textContent) == null ? void 0 : _e.trim()) || "未知作者";
      this.description = "";
      this.bookUrl = CURRENT_URL.href;
      this.targetEncoding = _unsafeWindow.targetEncoding;
      this.nav_toc = [];
      this.Text = [];
      this.Images = [];
      this.ImgLocation = [];
      this.ePubEidt = isEditingMode;
      this.ePubEidtDone = false;
      this.descriptionXhrInitiated = false;
      this.thumbnailImageAdded = false;
      this.isDownloadAll = downloadAllVolumes;
      this.XHRManager = Object.create(XHRDownloadManager);
      this.XHRManager.init(this);
      this.logger = UILogger;
      this.totalTasksAdded = 0;
      this.tasksCompletedOrSkipped = 0;
      this.XHRFail = false;
      this.VOLUME_ID_PREFIX = VOLUME_ID_PREFIX;
    }
    /**
     * 启动下载和构建流程
     * @param {HTMLElement} eventTarget - 触发下载的DOM元素 (用于单卷下载时定位)
     */
    start(eventTarget) {
      this.logger.clearLog();
      this.refreshProgress(this, "开始处理书籍...");
      this._loadExternalImageConfigs();
      const volumeElements = this.isDownloadAll ? Array.from(document.querySelectorAll(".vcss")) : [eventTarget.closest("td.vcss")];
      if (volumeElements.some((el) => !el)) {
        this.logger.logError("未能确定下载目标分卷,请检查页面结构。");
        const buildBtn = document.getElementById("EidterBuildBtn");
        if (buildBtn)
          buildBtn.disabled = false;
        return;
      }
      volumeElements.forEach((vcss, index) => {
        var _a, _b, _c, _d;
        const volumeName = (_b = (_a = vcss.childNodes[0]) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim();
        const volumePageId = vcss.getAttribute("vid");
        const firstChapterLink = (_d = (_c = vcss.parentElement) == null ? void 0 : _c.nextElementSibling) == null ? void 0 : _d.querySelector('a[href*=".htm"]');
        const firstChapterHref = firstChapterLink == null ? void 0 : firstChapterLink.getAttribute("href");
        const firstChapterId = firstChapterHref ? firstChapterHref.split(".")[0] : null;
        if (!volumeName || !volumePageId || !firstChapterId) {
          this.logger.logWarn(`分卷 ${index + 1} 信息不完整 (名称: ${volumeName}, 页面ID: ${volumePageId}, 首章ID: ${firstChapterId}),跳过此卷。`);
          return;
        }
        const volumeDomId = `${this.VOLUME_ID_PREFIX}_${index}`;
        const volumeHref = `${volumeDomId}.xhtml`;
        const navTocEntry = {
          volumeName,
          vid: volumePageId,
          // 实际的卷标识,用于ImgLocation等
          volumeID: volumeDomId,
          volumeHref,
          chapterArr: []
          // 章节列表稍后填充
        };
        this.nav_toc.push(navTocEntry);
        const textEntry = {
          path: `Text/${volumeHref}`,
          content: "",
          // 稍后填充
          id: volumeDomId,
          vid: volumePageId,
          // 关联到实际卷标识
          volumeName,
          navToc: navTocEntry
          // 引用,方便处理
        };
        this.Text.push(textEntry);
        const downloadUrl = `https://${DOWNLOAD_DOMAIN}/pack.php?aid=${this.aid}&vid=${firstChapterId}`;
        this.XHRManager.add({
          type: "webVolume",
          // 自定义类型
          url: downloadUrl,
          loadFun: (xhr) => VolumeLoader.loadWebVolumeText(xhr),
          // 使用VolumeLoader的方法
          VolumeIndex: index,
          // 用于在回调中定位nav_toc和Text中的条目
          data: { vid: volumePageId, vcssText: volumeName, Text: textEntry },
          // 传递给处理函数的信息
          bookInfo: this,
          // 传递EpubBuilderCoordinator实例
          isCritical: true
          // 卷内容是关键任务
        });
      });
      if (this.Text.length === 0) {
        this.refreshProgress(this, "没有有效的分卷被添加到下载队列。");
        const buildBtn = document.getElementById("EidterBuildBtn");
        if (buildBtn)
          buildBtn.disabled = false;
        return;
      }
      this.tryBuildEpub();
    }
    /**
     * 通知协调器一个任务已完成 (由 XHRManager 调用)
     */
    handleTaskCompletion() {
      new Promise((resolve) => {
        setTimeout(() => {
          resolve();
        }, 1e3);
      }).then(() => {
        _unsafeWindow._isUnderConstruction = false;
      });
    }
    /**
     * 尝试触发ePub构建流程
     * 只有当所有下载任务完成且没有关键失败时才会真正构建
     */
    tryBuildEpub() {
      EpubFileBuilder.build(this).then((result) => {
        if (!result || this.XHRFail) {
          return;
        }
        this.refreshProgress(this, "ePub文件已成功生成。");
        this.logger.logInfo("ePub文件已成功生成。", result);
        const buildBtn = document.getElementById("EidterBuildBtn");
        if (buildBtn)
          buildBtn.disabled = false;
      }).finally(() => {
        this.handleTaskCompletion();
      });
    }
    /**
     * 更新进度显示和日志 (适配旧的调用方式)
     * @param {EpubBuilderCoordinator} instance - 协调器实例 (通常是 this)
     * @param {string} [message] - 日志消息
     */
    refreshProgress(instance, message) {
      this.logger.updateProgress(instance, message);
    }
    /**
     * 从 unsafeWindow.ImgLocationCfgRef 加载外部图片配置
     */
    _loadExternalImageConfigs() {
      if (Array.isArray(_unsafeWindow.ImgLocationCfgRef) && _unsafeWindow.ImgLocationCfgRef.length > 0) {
        let loadedCount = 0;
        _unsafeWindow.ImgLocationCfgRef.forEach((cfg) => {
          if (cfg.UID === EPUB_EDITOR_CONFIG_UID && cfg.aid === this.aid && Array.isArray(cfg.ImgLocation) && cfg.ImgLocation.length > 0) {
            cfg.ImgLocation.forEach((loc) => {
              if (loc.vid && loc.spanID && loc.imgID) {
                if (!this.ImgLocation.some(
                  (existing) => existing.vid === loc.vid && existing.spanID === loc.spanID && existing.imgID === loc.imgID
                )) {
                  this.ImgLocation.push({
                    vid: String(loc.vid),
                    // 确保类型一致
                    spanID: String(loc.spanID),
                    imgID: String(loc.imgID)
                  });
                  loadedCount++;
                }
              }
            });
          }
        });
        if (loadedCount > 0) {
          this.refreshProgress(this, `已加载 ${loadedCount} 条来自书评的插图位置配置。`);
        }
        _unsafeWindow.ImgLocationCfgRef = [];
      }
    }
  }
  function initializeUserScript() {
    if (typeof _unsafeWindow.OpenCC !== "undefined" && typeof _unsafeWindow.translateButtonId !== "undefined") {
      OpenCCConverter.init(_unsafeWindow);
    } else {
      console.warn("OpenCC或页面翻译环境未准备好,增强的简繁转换未初始化。");
    }
    if (typeof _unsafeWindow.article_id === "undefined") {
      console.log("非书籍或目录页面,脚本主要功能不激活。");
      initializeReviewPageFeatures();
      return;
    }
    UILogger.init();
    if (typeof _unsafeWindow.chapter_id === "undefined" || _unsafeWindow.chapter_id === null || _unsafeWindow.chapter_id === "0") {
      if (document.querySelector("#title") && document.querySelectorAll(".vcss").length > 0) {
        addDownloadButtonsToCatalogPage();
      } else {
        console.log("页面缺少chapter_id,且不像是标准目录页,脚本核心功能不激活。");
      }
    } else {
      handleContentPage();
    }
    initializeReviewPageFeatures();
    loadConfigFromUrlIfPresent();
  }
  function addDownloadButtonsToCatalogPage() {
    const titleElement = document.getElementById("title");
    if (!titleElement)
      return;
    const bookTitle = titleElement.textContent.trim();
    const targetCharset = _unsafeWindow.targetEncoding === "1" ? "big5" : "utf8";
    const txtHref = `https://${DOWNLOAD_DOMAIN}/down.php?type=${targetCharset}&id=${_unsafeWindow.article_id}&fname=${encodeURIComponent(bookTitle)}`;
    const txtTitle = `全本文本下载(${targetCharset})`;
    const allTxtLink = createTxtDownloadButton(txtTitle, txtHref);
    titleElement.appendChild(allTxtLink);
    const epubAllButton = createDownloadButton(" ePub下载(全本)", false, true);
    const epubAllEditButton = createDownloadButton(" (调整插图)", true, true);
    titleElement.appendChild(epubAllButton);
    titleElement.appendChild(epubAllEditButton);
    const aEleSubEpub = createTxtDownloadButton(" 分卷ePub下载(全本)", "javascript:void(0);", true);
    aEleSubEpub.className = "DownloadAllSub";
    aEleSubEpub.addEventListener("click", (e) => loopDownloadSub());
    titleElement.append(aEleSubEpub);
    document.querySelectorAll("td.vcss").forEach((vcssCell) => {
      var _a, _b;
      const volumeName = (_b = (_a = vcssCell.childNodes[0]) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim();
      const volumeId = vcssCell.getAttribute("vid");
      if (!volumeName || !volumeId)
        return;
      const txtHref2 = `https://${DOWNLOAD_DOMAIN}/packtxt.php?aid=${_unsafeWindow.article_id}&vid=${volumeId}&aname=${encodeURIComponent(bookTitle)}&vname=${encodeURIComponent(volumeName)}&charset=${targetCharset.replace("utf8", "utf-8")}`;
      const txtTitle2 = ` 文本下载(${targetCharset.replace("utf8", "utf-8")})`;
      const volTxtLink = createTxtDownloadButton(txtTitle2, txtHref2);
      vcssCell.appendChild(volTxtLink);
      const epubVolButton = createDownloadButton(" ePub下载(本卷)", false, false);
      const epubVolEditButton = createDownloadButton(" (调整插图)", true, false);
      vcssCell.appendChild(epubVolButton);
      vcssCell.appendChild(epubVolEditButton);
    });
  }
  function createTxtDownloadButton(title, href, otherType = false) {
    const button = document.createElement("a");
    button.href = href;
    button.textContent = title;
    button.style.marginLeft = "5px";
    button.style.display = "inline-block";
    button.style.padding = "5px 10px";
    button.style.textDecoration = "none";
    button.style.borderRadius = "3px";
    button.style.cursor = "pointer";
    button.style.marginLeft = "5px";
    button.style.fontSize = "14px";
    button.style.lineHeight = "normal";
    if (otherType) {
      button.style.borderColor = "#ffe0b2";
      button.style.backgroundColor = "#fff8e1";
      button.style.color = "#fb602d";
    } else {
      button.style.borderColor = "#00bcd4";
      button.style.backgroundColor = "#b2ebf2";
      button.style.color = "#0047a7";
    }
    return button;
  }
  function createDownloadButton(text, isEditMode, isDownloadAll) {
    const button = document.createElement("a");
    button.href = "javascript:void(0);";
    button.textContent = text;
    button.style.display = "inline-block";
    button.style.padding = "5px 10px";
    button.style.border = "1px solid #ccc";
    button.style.backgroundColor = "#f0f0f0";
    button.style.color = "#333";
    button.style.textDecoration = "none";
    button.style.borderRadius = "3px";
    button.style.cursor = "pointer";
    button.style.marginLeft = "5px";
    button.style.fontSize = "14px";
    button.style.lineHeight = "normal";
    if (isEditMode) {
      button.style.borderColor = "#ff9800";
      button.style.backgroundColor = "#fff3e0";
      button.style.color = "#e65100";
      button.className = "";
      button.classList.add("EditMode");
    } else if (isDownloadAll) {
      button.style.borderColor = "#4caf50";
      button.style.backgroundColor = "#e8f5e9";
      button.style.color = "#1b5e20";
      button.className = "";
      button.classList.add("DownloadAll");
    } else {
      button.className = "ePubSub";
    }
    button.addEventListener("click", (event) => {
      event.target.style.pointerEvents = "none";
      event.target.style.opacity = "0.6";
      event.target.style.color = "#aaa";
      const coordinator = new EpubBuilderCoordinator(isEditMode, isDownloadAll);
      coordinator.start(event.target);
    });
    return button;
  }
  function loopDownloadSub() {
    const elements = document.querySelectorAll("a.ePubSub");
    const linksArray = Array.from(elements);
    const delayBetweenClicks = 5e3;
    const checkInterval = 5e3;
    const constructionTimeout = 6e4;
    UILogger.logInfo("循环下载分卷ePub(全本)...");
    function processElement(index) {
      if (index >= linksArray.length) {
        UILogger.logInfo("所有分卷下载任务处理完毕。");
        return;
      }
      const currentLink = linksArray[index];
      UILogger.logInfo(`开始处理链接: ${currentLink.href} (索引: ${index})`);
      checkConstructionStatus(index, Date.now());
    }
    function checkConstructionStatus(index, startTime) {
      const currentTime = Date.now();
      const elapsedTime = currentTime - startTime;
      const isUnderConstruction = typeof _unsafeWindow !== "undefined" && _unsafeWindow._isUnderConstruction;
      if (isUnderConstruction) {
        if (elapsedTime > constructionTimeout) {
          const errorMessage = `处理链接超时(${constructionTimeout / 1e3}s): ${linksArray[index].href} (索引: ${index}),跳过此元素。`;
          UILogger.logError(errorMessage);
          processElement(index + 1);
        } else {
          UILogger.logInfo(`检测到正在构建状态,已等待 ${elapsedTime / 1e3}s,${checkInterval / 1e3}秒后将再次检查...`);
          setTimeout(() => checkConstructionStatus(index, startTime), checkInterval);
        }
      } else {
        UILogger.logInfo("未处于构建状态,点击当前链接。");
        linksArray[index].click();
        UILogger.logInfo(`Clicked link: ${linksArray[index].href}`);
        setTimeout(() => processElement(index + 1), delayBetweenClicks);
      }
    }
    processElement(0);
  }
  function handleContentPage() {
    const contentMain = document.getElementById("contentmain");
    const contentDiv = document.getElementById("content");
    if (!contentMain || !contentDiv)
      return;
    const isCopyrightRestricted = contentMain.firstElementChild && contentMain.firstElementChild.tagName === "SPAN" && contentMain.firstElementChild.textContent.trim().toLowerCase() === "null";
    if (isCopyrightRestricted && typeof _unsafeWindow.chapter_id !== "undefined" && _unsafeWindow.chapter_id !== null && _unsafeWindow.chapter_id !== "0") {
      UILogger.logInfo("检测到版权限制页面,尝试通过App接口加载内容...");
      const bookInfoForReading = {
        aid: _unsafeWindow.article_id,
        targetEncoding: _unsafeWindow.targetEncoding,
        // 传递页面当前目标编码
        logger: UILogger,
        // 传递日志记录器
        refreshProgress: (info, msg) => UILogger.updateProgress(info, msg)
        // 适配旧的refreshProgress
      };
      AppApiService.fetchChapterForReading(bookInfoForReading, _unsafeWindow.chapter_id, contentDiv, _unsafeWindow.translateBody);
    }
  }
  function initializeReviewPageFeatures() {
    if (!CURRENT_URL.pathname.includes("/modules/article/"))
      return;
    document.querySelectorAll(".jieqiCode").forEach((codeElement) => {
      var _a, _b;
      const yidAnchor = (_a = codeElement.closest("table")) == null ? void 0 : _a.querySelector('a[name^="y"]');
      const yid = yidAnchor == null ? void 0 : yidAnchor.getAttribute("name");
      const reviewId = CURRENT_URL.searchParams.get("rid");
      const pageNum = CURRENT_URL.searchParams.get("page") || "1";
      if (reviewId && yid) {
        try {
          const configText = codeElement.textContent;
          const parsedConfig = JSON.parse(configText.replace(/\s/g, "").replace(/^\uFEFF/, ""));
          if (parsedConfig && parsedConfig.UID === EPUB_EDITOR_CONFIG_UID && parsedConfig.aid && parsedConfig.pathname && (parsedConfig.ImgLocationBase64 || Array.isArray(parsedConfig.ImgLocation) && parsedConfig.ImgLocation.length > 0)) {
            const titleDiv = (_b = yidAnchor.closest("tr")) == null ? void 0 : _b.querySelector("td > div");
            if (titleDiv) {
              const useConfigLink = document.createElement("a");
              useConfigLink.textContent = "[使用此插图配置生成ePub]";
              useConfigLink.style.color = "fuchsia";
              useConfigLink.style.marginRight = "10px";
              useConfigLink.href = `${CURRENT_URL.origin}${parsedConfig.pathname}?rid=${reviewId}&page=${pageNum}&yid=${yid}&CfgRef=1#title`;
              titleDiv.insertBefore(useConfigLink, titleDiv.firstChild);
            }
          }
        } catch (e) {
        }
      }
    });
  }
  _unsafeWindow.ImgLocationCfgRef = _unsafeWindow.ImgLocationCfgRef || [];
  async function loadConfigFromUrlIfPresent() {
    var _a;
    const urlParams = CURRENT_URL.searchParams;
    if (urlParams.get("CfgRef") !== "1")
      return;
    const rid = urlParams.get("rid");
    const page = urlParams.get("page");
    const yidToLoad = urlParams.get("yid");
    if (!rid || !yidToLoad)
      return;
    const reviewPageUrl = `${CURRENT_URL.origin}/modules/article/reviewshow.php?rid=${rid}&page=${page || 1}`;
    UILogger.init();
    UILogger.logInfo(`尝试从书评页加载插图配置...`);
    try {
      const response = await gmXmlHttpRequestAsync({ method: "GET", url: reviewPageUrl, timeout: XHR_TIMEOUT_MS });
      if (response.status === 200) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(cleanXmlIllegalChars(response.responseText), "text/html");
        const targetAnchor = doc.querySelector(`a[name="${yidToLoad}"]`);
        const codeElement = (_a = targetAnchor == null ? void 0 : targetAnchor.closest("table")) == null ? void 0 : _a.querySelector(".jieqiCode");
        if (codeElement) {
          const configText = codeElement.textContent;
          const parsedConfig = JSON.parse(configText.replace(/\s/g, "").replace(/^\uFEFF/, ""));
          if (parsedConfig.ImgLocationBase64) {
            try {
              const zip = new JSZip();
              await zip.load(parsedConfig.ImgLocationBase64, { base64: true });
              const imgLocFile = zip.file(IMG_LOCATION_FILENAME);
              if (imgLocFile) {
                const imgLocJson = await imgLocFile.async("string");
                parsedConfig.ImgLocation = JSON.parse(imgLocJson);
                delete parsedConfig.ImgLocationBase64;
              } else {
                throw new Error(`压缩包中未找到 ${IMG_LOCATION_FILENAME} 文件。`);
              }
            } catch (zipErr) {
              throw new Error(`解压或解析配置压缩包失败: ${zipErr.message}`);
            }
          }
          if (parsedConfig && parsedConfig.UID === EPUB_EDITOR_CONFIG_UID && parsedConfig.aid && Array.isArray(parsedConfig.ImgLocation) && parsedConfig.ImgLocation.length > 0) {
            _unsafeWindow.ImgLocationCfgRef.push(parsedConfig);
            UILogger.logInfo(`成功加载来自书评的 ${parsedConfig.ImgLocation.length} 条插图位置配置。现在可以点击下载按钮了。`);
            const titleElem = document.getElementById("title");
            if (titleElem) {
              const notice = document.createElement("p");
              notice.style.color = "green";
              notice.textContent = `提示:来自书评的插图配置已加载,请点击相应的“ePub下载(调整插图)”按钮开始。`;
              titleElem.parentNode.insertBefore(notice, titleElem.nextSibling);
            }
          } else {
            UILogger.logError(`书评中的配置无效或不完整。`);
          }
        } else {
          UILogger.logError(`在书评页未能找到对应的配置代码块。`);
        }
      } else {
        UILogger.logError(`下载书评配置失败,状态码: ${response.status}`);
      }
    } catch (error) {
      UILogger.logError(`加载或解析书评配置时出错: ${error.message}`);
      console.error("加载书评配置错误:", error);
    }
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", initializeUserScript);
  } else {
    initializeUserScript();
  }

})(FileSaver, JSZip);