☰

X岛-EX

X岛-EX 网页端增强,移动端般的浏览体验:快捷切换饼干-发送前二次确认 / 添加页首页码 / 关闭图片水印 / 预览真实饼干 / 隐藏无标题-无名氏-版规 / 显示外部图床 / 自动刷新饼干 toast提示 / 无缝翻页-自动翻页 / 默认原图+控件 / 新标签打开串 / 优化引用弹窗 / 拓展引用格式 / 当页回复编号 / 扩展坞增强 / 拦截回复中间页 / 颜文字拓展 / 高亮PO主 / 发串UI调整 / 『分组标记饼干』 / 『屏蔽饼干』 / 『只看饼干』 / 『屏蔽关键词』- 隐藏-折叠 / 增强X岛匿名版 / 板块页快速回复 / 展开板块页长串 / 野生搜索酱 / unvcode-零宽空格模式 / 侧边栏收起 / 图片显示模式 / 图片自动压缩-非法图像格式(无GCT)GIF重编码 / 链接自动识别 / 使用数据-设置项-导入导出-剪贴板文件 / 常用串 / 浏览历史 / 发言历史 / 移动端订阅 / 阅图模式 。

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         X岛-EX
// @namespace    https://github.com/SayaGoodBye/nmbxd-EX
// @version      4.3.0
// @description  X岛-EX 网页端增强,移动端般的浏览体验:快捷切换饼干-发送前二次确认 / 添加页首页码 / 关闭图片水印 / 预览真实饼干 / 隐藏无标题-无名氏-版规 / 显示外部图床 / 自动刷新饼干 toast提示 / 无缝翻页-自动翻页 / 默认原图+控件 / 新标签打开串 / 优化引用弹窗 / 拓展引用格式 / 当页回复编号 / 扩展坞增强 / 拦截回复中间页 / 颜文字拓展 / 高亮PO主 / 发串UI调整 / 『分组标记饼干』 / 『屏蔽饼干』 / 『只看饼干』 / 『屏蔽关键词』- 隐藏-折叠 / 增强X岛匿名版 / 板块页快速回复 / 展开板块页长串 / 野生搜索酱 / unvcode-零宽空格模式 / 侧边栏收起 / 图片显示模式 / 图片自动压缩-非法图像格式(无GCT)GIF重编码 / 链接自动识别 / 使用数据-设置项-导入导出-剪贴板文件 / 常用串 / 浏览历史 / 发言历史 / 移动端订阅 / 阅图模式 。
// @author       XY
// @match        https://*.nmbxd1.com/*
// @match        https://*.nmbxd.com/*
// @match        https://nmb-search.166666666.xyz/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @grant        GM_xmlhttpRequest
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_addStyle
// @grant        unsafeWindow
// @connect      nmbxd1.com
// @connect      www.nmbxd1.com
// @connect      nmbxd.com
// @connect      www.nmbxd.com
// @connect      nmb-search.166666666.xyz
// @connect      image.nmb.best
// @connect      api.nmb.best
// @connect      raw.githubusercontent.com
// @connect      cdn.jsdelivr.net
// @connect      fastly.jsdelivr.net
// @connect      update.greasyfork.org
// @connect      scriptcat.org
// @connect      code.jquery.com
// @connect      unpkg.com
// @connect      *
// 说明:* 通配允许 GM_xmlhttpRequest 访问任意域名,用于用户自定义 WebDAV 地址(无需手动逐域授权)
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/lib/index.js
// @require      https://unpkg.com/[email protected]/UPNG.js
// @icon         https://image.nmb.best/image/2026-06-03/6a1fcea41fad3.png
// @icon64       https://image.nmb.best/image/2026-06-03/6a1fced8e0e64.png
// @license      WTFPL
// @changelog    ### 新增\n\n- 新增导出功能,支持将串导出为Markdown/TXT文件\n- 新增饼干计时器,发送回复后提示当前饼干冷却时间\n- 新增被屏蔽串内的临时显示功能\n\n### 优化\n\n- 发送消息前对图像大小/格式进行预检,提前将图像压缩大小至2048KB以内或转化为合法格式之后再提交\n- 打开浏览历史/发言历史面板时自动聚焦搜索\n- 性能优化,减少在写入发言历史/发送消息后局部刷新的场景中发生的卡顿\n- 正文/标题/名称/E-mai拼音组词过程中不计算字数\n\n### 修复\n\n- 修复在弹出二次确认饼干时弹出“可能提交失败”toast提示的问题\n- 修复在部分场景下没有拓宽带图消息的问题\n
// @note         特别感谢:icon由9HrD12x设计并绘制 >>No.68765505
// @note         致谢:切饼代码移植自[XD-Enhance](https://greasyfork.org/zh-CN/scripts/438164-xd-enhance)
// @note         致谢:外部图床代码二改自[显示x岛图片链接指向的图片](https://greasyfork.org/zh-CN/scripts/546024-%E6%98%BE%E7%A4%BAx%E5%B2%9B%E5%9B%BE%E7%89%87%E9%93%BE%E6%8E%A5%E6%8C%87%E5%90%91%E7%9A%84%E5%9B%BE%E7%89%87)
// @note         致谢:完整移植[增强x岛匿名版](https://greasyfork.org/zh-CN/scripts/513156-%E5%A2%9E%E5%BC%BAx%E5%B2%9B%E5%8C%BF%E5%90%8D%E7%89%88)
// @note         致谢:部分功能移植自[X岛-揭示板的增强型体验](https://greasyfork.org/zh-CN/scripts/497875-x%E5%B2%9B-%E6%8F%AD%E7%A4%BA%E6%9D%BF%E7%9A%84%E5%A2%9E%E5%BC%BA%E5%9E%8B%E4%BD%93%E9%AA%8C#%E8%BF%9E%E6%8E%A5%E7%9B%B4%E6%8E%A5%E8%B7%B3%E8%BD%AC)
// @note         致谢:来自4sYbzEX的搜索服务[野生搜索酱](https://www.nmbxd.com/t/64792841)
// @note         致谢:来自acVMxuv的[侧边栏优化](https://greasyfork.org/zh-CN/scripts/553143-x%E5%B2%9B%E4%BC%98%E5%8C%96%E5%B2%9B-%E4%BE%A7%E8%BE%B9%E6%A0%8F%E4%BC%98%E5%8C%96%E7%89%88)
// @run-at       document-start
// ==/UserScript==
/* global $, jQuery */

// 更新渠道
// @downloadURL https://update.greasyfork.org/scripts/531005/X%E5%B2%9B-EX.user.js
// @updateURL https://update.greasyfork.org/scripts/531005/X%E5%B2%9B-EX.meta.js
// @downloadURL https://scriptcat.org/scripts/code/6289/X%E5%B2%9B-EX.user.js
// @updateURL https://scriptcat.org/scripts/code/6289/X%E5%B2%9B-EX.meta.js

/* ==XDEX-EARLY-THEME-START== (独立于主 IIFE:首帧前即执行,不依赖 jQuery/GM 存储就绪) */
const XDEX_DARK_SESSION_KEY = 'xdexCustomDarkEnabled';
function xdexEarlyDarkEnabled() {
  try {
    const session = sessionStorage.getItem(XDEX_DARK_SESSION_KEY);
    if (session === 'true') return true;
    if (session === 'false') return false;
  } catch (e) {}
  try {
    return (GM_getValue('myScriptSettings', {}) || {}).enableCustomDarkTheme === 'dark';
  } catch (e) { return false; }
}
(function earlyCustomDarkTheme() {
  try {
    const boot = () => {
      try {
        const rootEl = document.documentElement;
        if (!rootEl) return;
        const drMode = rootEl.getAttribute('data-darkreader-mode') || rootEl.getAttribute('data-darkreader-scheme');
        if (drMode && drMode !== 'off') return;
        const extMarked = rootEl.classList.contains('xdex-custom-dark');
        if (!extMarked && !xdexEarlyDarkEnabled()) return;
        rootEl.classList.add('xdex-custom-dark');
        if (!document.getElementById('xdex-custom-theme')) {
          const style = document.createElement('style');
          style.id = 'xdex-custom-theme';
                      style.textContent = 'html.xdex-custom-dark,html.xdex-custom-dark body{background-color:#28292a!important;color:#d9d0d0!important}' +
              ':root.xdex-custom-dark #h-content,:root.xdex-custom-dark #h-content *,:root.xdex-custom-dark .h-threads-item,:root.xdex-custom-dark .h-threads-item *,:root.xdex-custom-dark .h-threads-item-reply-main,:root.xdex-custom-dark .h-preview-box,:root.xdex-custom-dark .h-preview-box *,:root.xdex-custom-dark #h-menu,:root.xdex-custom-dark #h-menu *,:root.xdex-custom-dark #h-bottom-nav,:root.xdex-custom-dark .qp-quote,:root.xdex-custom-dark .qp-quote *,:root.xdex-custom-dark #h-ref-view,:root.xdex-custom-dark #h-ref-view *,:root.xdex-custom-dark .kaomoji-panel,:root.xdex-custom-dark .kaomoji-panel *,:root.xdex-custom-dark .h-post-form,:root.xdex-custom-dark .h-post-form *,:root.xdex-custom-dark .xdex-post-history-thread,:root.xdex-custom-dark .xdex-post-history-thread *{background-color:#28292a!important;color:#d9d0d0!important}' +
              ':root.xdex-custom-dark #h-menu{background-color:#28292a!important;color:#e04747!important}' +
              ':root.xdex-custom-dark font[color="#789922"]{color:#b5d06d!important}';
          const mount = document.head || rootEl || document;
          mount.appendChild(style);
          if (!document.head) {
            const headObserver = new MutationObserver(() => {
              if (document.head) {
                headObserver.disconnect();
                if (style.isConnected) document.head.appendChild(style);
              }
            });
            headObserver.observe(rootEl, { childList: true });
          }
        }
      } catch (e) {}
    };
    if (!document.documentElement) {
      const rootMo = new MutationObserver(() => {
        if (document.documentElement) { rootMo.disconnect(); boot(); }
      });
      rootMo.observe(document, { childList: true, subtree: true });
      return;
    }
    boot();
  } catch (e) {}
})();
/* ==XDEX-EARLY-THEME-END== */
(function($){
  'use strict';
  /* --------------------------------------------------
   * tag 0. 通用与工具函数
   * -------------------------------------------------- */
  const VERSION = GM_info.script.version;
  const XDEX_SINGLETON_OWNER_DATASET_KEY = 'xdexSingletonOwner';
  const XDEX_SINGLETON_WAIT_MS = 100;
  function getXDexRuntimeInfo(){
      const declared = typeof globalThis !== 'undefined' ? globalThis.__xdexRuntime : null;
      if (declared && declared.kind === 'extension') {
          return Object.assign({ kind: 'extension' }, declared);
      }
      const scriptHandler = GM_info && (GM_info.scriptHandler || (GM_info.script && GM_info.script.handler)) || '';
      return {
          kind: 'userscript',
          scriptHandler: scriptHandler || 'unknown'
      };
  }
  (function earlyIconBtnStyle() {
    try {
      if (document.getElementById('xdex-icon-btn-global')) return;
      const style = document.createElement('style');
      style.id = 'xdex-icon-btn-global';
      style.textContent =
        '.xdex-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:1px solid var(--xdex-sp-border,#bfa58f);border-radius:8px;background:var(--xdex-sp-panel-bg,#F0E0D6);cursor:pointer;color:inherit;box-sizing:border-box;}' +
        '.xdex-icon-btn:hover{border-color:#00ffcc !important;color:#00ffcc !important;}' +
        '.xdex-icon-btn:active{border-color:#00b386 !important;color:#00b386 !important;}' +
        '.xdex-icon-btn svg{display:block;}' +
        '.xdex-clear-image-btn:hover{border-color:#EE0000 !important;color:#EE0000 !important;}' +
        '#btn_sp_fullExport_reset:hover{border-color:#EE0000 !important;color:#EE0000 !important;}' +
        '.xdex-reply-mode-btn{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:1px solid var(--xdex-sp-border,#bfa58f);border-radius:8px;background:var(--xdex-sp-panel-bg,#F0E0D6);cursor:pointer;color:inherit;box-sizing:border-box;flex:0 0 auto;}' +
        '.xdex-reply-mode-btn:hover{border-color:#00ffcc !important;color:#00ffcc !important;}' +
        '.xdex-reply-mode-btn svg{display:block;}' +
        ':root.xdex-custom-dark .xdex-reply-mode-btn,:root.xdex-darkreader-active .xdex-reply-mode-btn{background:#2b2c2d;border-color:#5a4d42;color:#d9d0d0;}' +
        ':root.xdex-custom-dark .xdex-reply-mode-btn:hover,:root.xdex-darkreader-active .xdex-reply-mode-btn:hover{border-color:#00ffcc !important;color:#00ffcc !important;}' +
        '.xdex-reply-mode-btn.js-reset:hover{border-color:#EE0000 !important;color:#EE0000 !important;}' +
        ':root.xdex-custom-dark .xdex-reply-mode-btn.js-reset:hover,:root.xdex-darkreader-active .xdex-reply-mode-btn.js-reset:hover{border-color:#EE0000 !important;color:#EE0000 !important;}' +
        ':root.xdex-custom-dark .xdex-icon-btn,:root.xdex-darkreader-active .xdex-icon-btn{background:#2b2c2d;border-color:#555;color:#d9d0d0;}' +
        ':root.xdex-custom-dark .qp-overlay-quote .h-threads-info .h-threads-info-title{color:#e85248 !important;}' +
        ':root.xdex-custom-dark .qp-overlay-quote .h-threads-item-reply-main{background:transparent !important;}' +
        ':root.xdex-custom-dark .qp-overlay-quote .h-threads-info,:root.xdex-custom-dark .qp-overlay-quote .h-threads-info *,:root.xdex-custom-dark .qp-overlay-quote .h-threads-content,:root.xdex-custom-dark .qp-overlay-quote .h-threads-content *{color:#d9d0d0 !important;}' +
        ':root.xdex-custom-dark .qp-overlay-quote .h-threads-tips.uk-text-danger{color:#e85248 !important;}' +
        ':root.xdex-custom-dark .qp-overlay-quote font[color="#789922"]{color:#b5d06d !important;}' +
        ':root.xdex-custom-dark .qp-overlay-quote a,:root.xdex-custom-dark .qp-overlay-quote .h-threads-content a,:root.xdex-custom-dark .qp-overlay-quote .h-threads-info a{color:#3e8eec !important;}' +
        ':root.xdex-custom-dark .kaomoji-item:hover{background:#3a3d40 !important;}' +
        '@keyframes xdex-icon-sync-spin{to{transform:rotate(360deg)}}' +
        '.xdex-icon-loading .xdex-icon-sync{animation:xdex-icon-sync-spin .8s linear infinite}' +
        '.xdex-board-refresh-btn:disabled{opacity:.55;cursor:default}' +
        '.xdex-edit-default-btn{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:none;cursor:pointer;color:inherit;flex-shrink:0;vertical-align:middle;margin-right:2px;}' +
        '.xdex-edit-default-btn:hover{color:#00ffcc !important;}' +
        '#xdex-image-viewer.xdex-iv-no-sep .xv-page-separator{display:none !important;}' +
        ':root.xdex-custom-dark .kaomoji-item.kaomoji-active{background:#3a3d40 !important;}';
      (document.head || document.documentElement).appendChild(style);
      console.log('[xdex-theme-early] icon-btn global style injected');
    } catch (e) {}
  })();
  function shouldExitForXDexSingleton(runtimeInfo){
      const root = document.documentElement;
      const owner = root && root.dataset ? root.dataset[XDEX_SINGLETON_OWNER_DATASET_KEY] : '';
      return owner === 'extension' && (!runtimeInfo || runtimeInfo.kind !== 'extension');
  }
  function cat_version(){
      console.log('[version]:', VERSION);
  }
  const XDEX_RUNTIME = getXDexRuntimeInfo();
  function getXDexGmStorageReady(){
      const ready = typeof globalThis !== 'undefined' ? globalThis.__xdexGmStorageReady : null;
      return ready && typeof ready.then === 'function' ? ready : Promise.resolve();
  }
  const XDEX_GM_STORAGE_READY = getXDexGmStorageReady();
  function scheduleXDexStartup(){
      if (shouldExitForXDexSingleton(XDEX_RUNTIME)) return;
      const startAfterStorageReady = () => {
          if (shouldExitForXDexSingleton(XDEX_RUNTIME)) return;
          startXDexRuntime();
      };
      if (XDEX_RUNTIME.kind !== 'extension') {
        setTimeout(() => {
          if (shouldExitForXDexSingleton(XDEX_RUNTIME)) return;
          XDEX_GM_STORAGE_READY.then(() => {
              startAfterStorageReady();
          }, () => {
              startAfterStorageReady();
          });
        }, XDEX_SINGLETON_WAIT_MS);
        return;
      }
      XDEX_GM_STORAGE_READY.then(() => {
          startAfterStorageReady();
      }, () => {
          startAfterStorageReady();
      });
  }
  function startXDexRuntime(){
      cat_version();
      console.log('[runtime]:', XDEX_RUNTIME.kind, XDEX_RUNTIME);
  function gmRequest(url, responseType = 'text', headers = null) {
    return new Promise((resolve, reject) => {
      const request = {
        method: 'GET',
        url,
        responseType,
        onload: (resp) => {
          if (resp.status >= 200 && resp.status < 300) {
            resolve(resp);
          } else {
            reject(new Error(`HTTP ${resp.status} ${url}`));
          }
        },
        onerror: () => reject(new Error(`Request failed: ${url}`)),
        ontimeout: () => reject(new Error(`Request timeout: ${url}`))
      };
      if (headers) request.headers = headers;
      GM_xmlhttpRequest(request);
    });
  }
  const toastQueue = [];
  let isShowing = false;
  let refreshStatusGeneration = 0;
  function beginRefreshStatus() {
    refreshStatusGeneration += 1;
    return refreshStatusGeneration;
  }
  // ============ 页面类型 / 页码通用工具(PageType) ============
  const PageType = {
    getPathname() { return location.pathname || ''; },
    isThreadPage(loose) {
      if (/^\/Forum\/po\/id\/\d+/.test(this.getPathname())) return true;
      const re = loose === false ? /^\/t\/\d{6,8}(?:\/|$)/ : /^\/t\/\d{4,}(?:\/|$)/;
      return re.test(this.getPathname());
    },
    isPOThreadPage() { return /^\/Forum\/po\/id\/\d+/.test(this.getPathname()); },
    isBoardPage() { return /^\/f\//.test(this.getPathname()); },
    isTimelinePage() { return /^\/Forum\/timeline\/id\/\d+/.test(this.getPathname()); },
    isTimelineAnyPage() { return /\/Forum\/timeline\//i.test(this.getPathname()); },
    isTimelineLikePage() { return /\/Forum\/timeline\/id\/\d+/.test(this.getPathname()) || /\/timeline|\/feed/.test(this.getPathname()); },
    isBoardOrTimelinePage() { return this.isBoardPage() || this.isTimelinePage(); },
    isSpecialBoard() { return /^\/f\/(?:值班室|测试)(?:\/|$)/.test(decodeURIComponent(this.getPathname())); },
    getThreadId(loose) {
      const p = this.getPathname();
      const re = loose === false ? /^\/t\/(\d{6,8})(?:\/|$)/ : /^\/t\/(\d{4,})(?:\/|$)/;
      const m = p.match(re) || p.match(/^\/Forum\/po\/id\/(\d+)/);
      return m ? m[1] : '';
    },
    getPageNum() {
      // ?page= 优先,再回退路径 /page/N(与多数调用点语义一致)
      const q = parseInt(new URL(location.href, location.origin).searchParams.get('page') || '', 10);
      if (q > 0) return q;
      const m = this.getPathname().match(/\/page\/(\d+)(?:\.html)?$/);
      return m ? Math.max(1, parseInt(m[1], 10)) : 1;
    },
    getPathPageNum() {
      // 路径 /page/N 优先,再回退 ?page=(阅图模式语义)
      const m = this.getPathname().match(/\/page\/(\d+)(?:\.html)?$/);
      if (m) return Math.max(1, parseInt(m[1], 10));
      const q = parseInt(new URL(location.href, location.origin).searchParams.get('page') || '', 10);
      return q > 0 ? q : 1;
    },
    getTimelineInfo() {
      const m = this.getPathname().match(/^\/Forum\/timeline\/id\/(\d+)(?:\/page\/(\d+)(?:\.html)?)?/i);
      return m ? { id: m[1], page: m[2] ? parseInt(m[2], 10) : 1 } : null;
    }
  };
  function isCurrentRefreshStatus(generation) {
    return generation === refreshStatusGeneration;
  }
  function showRefreshStatus(msg, duration = 900, type = '') {
    // type='error':红框 + 驻留;type='hold':无红框 + 驻留(如「无更新」需留观但非错误);
    // 复用 refresh-status 节点,后续任何刷新状态提示(正在刷新/已更新)会自动顶掉它
    return toast(msg, duration, { queue: false, key: 'refresh-status', type });
  }

  function toast(msg, duration = 1800, options = {}) {
    if (options.type === 'error' || options.type === 'hold') {
      // ★ 错误/长驻提示强制走立即通道(不排队):error 红框驻留,hold 无红框驻留(见 showImmediateToast)
      showImmediateToast(msg, duration, options.key, options.type);
      return;
    }
    if (options.queue === false) {
      showImmediateToast(msg, duration, options.key);
      return;
    }
    toastQueue.push({ msg, duration });
    if (!isShowing) showNextToast();
  }
  function showNextToast() {
    if (toastQueue.length === 0) {
      isShowing = false;
      return;
    }
    isShowing = true;
    const { msg, duration } = toastQueue.shift();
    console.log('[toast]', msg);
    // ✅ 每次创建一个新的 toast 节点
    // 多行支持:white-space:pre-wrap 保留 \n 与缩进(同时仍自动换行长行);
    // 文本一律走 .text() 转义,避免消息里的 < > 被当成 HTML(此前是裸插值,且全仓无调用方传 HTML)
    const $t = $(`<div class="ae-toast" style="
      position:fixed;top:10px;left:50%;transform:translateX(-50%);
      background:rgba(0,0,0,.75);color:#fff;padding:8px 18px;
      border-radius:5px;z-index:10099;display:none;font-size:14px;
      white-space:pre-wrap;max-width:min(92vw,600px);text-align:left;line-height:1.45;
      word-break:break-word;overflow-wrap:anywhere;"></div>`).text(msg);
    $('body').append($t);
    $t.fadeIn(240).delay(duration).fadeOut(240, () => {
      $t.remove();     // ✅ 动画结束后删除节点
      showNextToast(); // ✅ 显示下一个
    });
  }
  function showImmediateToast(msg, duration = 900, key = 'default', type = '') {
    const safeKey = String(key || 'default').replace(/[^a-z0-9_-]/gi, '-');
    if (safeKey === 'refresh-status') console.log('[toast]', msg); // 刷新状态提示同步输出到控制台(对齐通用 toast 路径)
    let $t = $(`#xdex-immediate-toast-${safeKey}`);
    if (!$t.length) {
      $t = $(`<div id="xdex-immediate-toast-${safeKey}" class="ae-toast" style="
        position:fixed;top:10px;left:50%;transform:translateX(-50%);
        background:rgba(0,0,0,.75);color:#fff;padding:8px 18px;
        border-radius:5px;z-index:10099;display:none;font-size:14px;
        white-space:pre-wrap;max-width:min(92vw,600px);text-align:left;line-height:1.45;
        word-break:break-word;overflow-wrap:anywhere;"></div>`);
      $('body').append($t);
    }
    if (safeKey === 'refresh-status') {
      $t.css({
        background: '#242424',
        boxShadow: '0 4px 16px rgba(0,0,0,.55)',
        fontWeight: 'bold',
      });
    }
    const el = $t[0];
    if (type === 'error') {
      // ★ 错误提示:红框 + 不自动消失,驻留到下次用户交互(点击/键盘/焦点切换),30s 保险上限
      $t.css({
        border: '1px solid #e74c3c',
        boxShadow: '0 0 0 1px rgba(231,76,60,.4), 0 4px 16px rgba(0,0,0,.55)',
        fontWeight: 'bold',
      });
      el.__xdexToastError = true;
    } else if (type === 'hold') {
      // ★ 长驻提示:与 error 相同的驻留语义(无操作 30s / 操作后 5s 渐隐 / 同 key 顶掉 / 1s 武装期),
      // 但不加红框——用于「无更新」这类需要留观、又不应表现成错误的场景
      el.__xdexToastError = true;
    }
    // ★ 复用节点时先清理上一代的驻留监听/计时器:
    // 「错误 toast 可被下一条同为 error 的 toast 顶掉」由此实现——顶掉一刻起旧监听即刻失效,
    // 若等到下次交互才清理会造成 document 捕获相监听器 + 30s 计时器永久泄漏
    if (el.__xdexToastDismissCleanup) {
      el.__xdexToastDismissCleanup();
      if (el.__xdexToastError) {
        try { console.log('[xdex error toast] 关闭原因: 被同 key 的新 toast 顶掉'); } catch (e) {}
      }
      el.__xdexToastDismissCleanup = null;
    }
    // 上一轮是错误、本轮不是 error/hold → 清除遗留的红框/加粗样式(refresh-status 专属样式见上方重新设置,无冲突)
    if (el.__xdexToastError && type !== 'error' && type !== 'hold') {
      $t.css({ border: '', boxShadow: '', fontWeight: '' });
      if (safeKey === 'refresh-status') {
        $t.css({
          background: '#242424',
          boxShadow: '0 4px 16px rgba(0,0,0,.55)',
          fontWeight: 'bold',
        });
      }
    }
    el.__xdexToastError = (type === 'error' || type === 'hold');
    const seq = (Number(el.__xdexImmediateToastSeq) || 0) + 1;
    el.__xdexImmediateToastSeq = seq;
    $t.stop(true, false).text(msg).show();
    // 淡出被 stop(true,false) 打断时 style.opacity 会冻结在中间值,而 jQuery 的 .show() 不恢复不透明度,
    // 导致「上一条正在淡出时下发的新消息」整段偏淡甚至近乎不可见 → 显式复位
    $t.css('opacity', 1);
    if (type === 'error' || type === 'hold') {
      dismissOnNextInteraction($t, el, seq);
      return $t;
    }
    $t.delay(duration).fadeOut(160, () => {
      if (el.__xdexImmediateToastSeq === seq) $t.remove();
    });
    return $t;
  }

  // ★ 错误 toast 语义
  // 无操作驻留 30 秒(比普通 toast 的 1.8s 长得多,核心目的:提醒窗口拉长,避免一闪而过);
  // 用户发生键鼠操作后,进入 5 秒渐隐宽限(给足看清内容的时间,不是瞬间消失);
  // 挂载后 1 秒武装期内忽略交互(提交后的连续击键/点击不误关);同 key 新 error 顶掉旧 error。
  // ★ 算「操作」的动作:左键/中键/右键按下、滚轮滚动、任意按键——均为对页面本身的操作;
  // 单纯移动鼠标、窗口切换、输入法切换不算(不监听 pointermove/focusin)。
  const ERROR_TOAST_MAX_HOLD_MS = 30000;            // 无操作驻留上限
  const ERROR_TOAST_FADE_AFTER_INTERACT_MS = 5000;  // 操作后的渐隐宽限
  // ★ 挂载后 1 秒内的 pointerdown/keydown 不关闭 toast:
  // 「含有非法词语」等错误出现在提交动作之后,用户往往正在继续打字或点击(keydown/pointerdown
  // 紧随 toast 出现),若立即武装会把刚弹出的提示秒关——实测「消失得很快」的主因。
  const ERROR_TOAST_ARM_DELAY_MS = 1000;
  function dismissOnNextInteraction($t, el, seq) {
    const dismiss = (reason) => {
      // ★ 消失原因诊断:无论哪条路径关闭,都输出原因(排查「红框很快消失」用)
      try { console.log(`[xdex error toast] 关闭原因: ${reason}`); } catch (e) {}
      $t.css('opacity', 1); // fadeOut 需要从完全不透明起步
      $t.fadeOut(160, () => {
        if (el.__xdexImmediateToastSeq === seq) $t.remove();
      });
    };
    let holdTimer = 0;
    let fadeTimer = 0;
    let armed = false;
    let armTimer = 0;
    let interacted = false;
    let pointerH, keyH, wheelH; // 保存「已挂载」的包裹引用,移除时才能精确匹配
    const clearListeners = () => {
      if (pointerH) document.removeEventListener('pointerdown', pointerH, true);
      if (keyH) document.removeEventListener('keydown', keyH, true);
      if (wheelH) document.removeEventListener('wheel', wheelH, true);
      pointerH = keyH = wheelH = null;
    };
    const cleanup = () => {
      clearListeners();
      window.clearTimeout(holdTimer);
      window.clearTimeout(fadeTimer);
      window.clearTimeout(armTimer);
      // 节点级清理钩子已执行 → 清掉,避免下次复用重复触发
      if (el.__xdexToastDismissCleanup === cleanup) el.__xdexToastDismissCleanup = null;
    };
    // 交互后的 5 秒渐隐宽限:清监听与 30s 驻留,改由宽限计时器接管
    const startFadeGrace = () => {
      clearListeners();
      window.clearTimeout(holdTimer);
      fadeTimer = window.setTimeout(() => {
        dismiss(`${ERROR_TOAST_FADE_AFTER_INTERACT_MS / 1000} 秒宽限期满,渐隐`);
        cleanup();
      }, ERROR_TOAST_FADE_AFTER_INTERACT_MS);
    };
    // 传递 seq,比对代看守:若期间同 key 又来了新消息(含下一条 error 顶掉),本 toast 已不再是最新
    const guarded = (fn) => () => {
      if (!armed) return;            // 武装期内忽略(弹出的同一手势/连续击键不关)
      if (el.__xdexImmediateToastSeq !== seq) return;
      fn();
    };
    const onPointer = (e) => {
      // ★ 左键点击、中键按下(button===1,多为自动滚动/关闭标签页习惯动作)、右键按下(button===2,唤出菜单)
      // 都算「对本页操作」;其余键值(如浏览器前进/后退侧键)不计。
      if (e.button !== 0 && e.button !== 1 && e.button !== 2) return;
      if (interacted) return;
      interacted = true;
      startFadeGrace();
      const buttonName = e.button === 0 ? '左键' : e.button === 1 ? '中键' : '右键';
      console.log(`[xdex error toast] 检测到鼠标${buttonName}操作 → ${ERROR_TOAST_FADE_AFTER_INTERACT_MS / 1000} 秒后渐隐 (target=${e.target && e.target.tagName || '?'}, isTrusted=${e.isTrusted})`);
    };
    const onWheel = (e) => {
      // ★ 滚轮滚动也算「对本页操作」:阅读长串时最常用的就是滚动,不应要求必须左键点击。
      // 只在 NMBXD 页面内容上滚动才算(滚轮事件默认绑定在 document,不覆盖 DevTools 等外部区域)。
      if (interacted) return;
      interacted = true;
      startFadeGrace();
      console.log(`[xdex error toast] 检测到滚轮滚动 → ${ERROR_TOAST_FADE_AFTER_INTERACT_MS / 1000} 秒后渐隐 (deltaY=${e.deltaY}, isTrusted=${e.isTrusted})`);
    };
    const onKey = (e) => {
      if (interacted) return;
      interacted = true;
      startFadeGrace();
      console.log(`[xdex error toast] 检测到键盘按键 → ${ERROR_TOAST_FADE_AFTER_INTERACT_MS / 1000} 秒后渐隐 (key=${e.key || '?'}, isTrusted=${e.isTrusted})`);
    };
    pointerH = (e) => {
      if (!armed) return;
      if (el.__xdexImmediateToastSeq !== seq) return;
      onPointer(e);
    };
    wheelH = (e) => {
      if (!armed) return;
      if (el.__xdexImmediateToastSeq !== seq) return;
      onWheel(e);
    };
    keyH = (e) => {
      if (!armed) return;
      if (el.__xdexImmediateToastSeq !== seq) return;
      onKey(e);
    };
    // ★ 提前关闭只认「明确指向本页的操作」:pointerdown / keydown / wheel。
    // 不监听 focusin——发送失败后脚本恢复输入框焦点、输入法切换、Alt+Tab 回窗口等
    // 都会产生 focusin,会在用户还没看到提示时就把它关掉(实测「红框很快消失」的主因)。
    document.addEventListener('pointerdown', pointerH, true);
    document.addEventListener('keydown', keyH, true);
    document.addEventListener('wheel', wheelH, true);
    armTimer = window.setTimeout(() => { armed = true; }, ERROR_TOAST_ARM_DELAY_MS);
    // ★ 无操作 30 秒驻留上限(从挂载起算;交互后由 5s 渐隐宽限接管,本计时器被 startFadeGrace 清除)
    holdTimer = window.setTimeout(() => { dismiss(`无操作到达 ${ERROR_TOAST_MAX_HOLD_MS / 1000} 秒驻留上限,自动消失`); cleanup(); }, ERROR_TOAST_MAX_HOLD_MS);
    // 清理钩子挂到节点:showImmediateToast 复用它时能立刻停止旧监听/计时器(顶掉语义)
    el.__xdexToastDismissCleanup = cleanup;
  }
  const Utils = {
      // 逗号(中英)分隔,支持转义 \, \, \\
      strToList(s) {
        if (!s) return [];
        const list = [], esc = ',,\\';
        let cur = '';
        for (let i = 0; i < s.length; i++) {
          const ch = s[i];
          if (ch === '\\' && i + 1 < s.length && esc.includes(s[i+1])) {
            cur += s[++i];
          } else if (ch === ',' || ch === ',') {
            const t = cur.trim();
            if (t) list.push(t);
            cur = '';
          } else cur += ch;
        }
        const t = cur.trim();
        if (t) list.push(t);
        return [...new Set(list)];
      },
      cookieLegal: s => /^[A-Za-z0-9]{3,8}$/.test(s),
      cookieMatch: (cid,p) => cid.toLowerCase().includes(p.toLowerCase()),
      firstHit(txt,list) {
        return list.find(k=>txt.toLowerCase().includes(k.toLowerCase()))||null;
      },
      collapse($elem, hint) {
        if (!$elem.length || $elem.data('xdex-collapsed')) return;
        // 整表单不要在浮窗内被 early pass 再折成『回复/发串』按钮;
        // 但 .collapse-wrapper(标题/名称/E-mail 可选项)即使已在 .qp-body 内也必须允许折叠
        if ($elem.hasClass('qp-reply-form')) return;
        if ($elem.closest('.qp-body').length && !$elem.hasClass('collapse-wrapper')) return;
        const $icons = $elem.find('.h-threads-item-reply-icon');
        let nums = '';
        if ($icons.length) {
          const f = $icons.first().text();
          const l = $icons.last().text();
          nums = $icons.length>1 ? `${f}-${l} ` : `${f} `;
        }
        const cap = `${nums}${hint}`;
        const $ph = $(`
          <div class="xdex-placeholder xdex-generic-toggle" style="
            padding:6px 10px;background:#fafafa;color:#888;
            border:1px dashed #bbb;margin-bottom:3px;cursor:pointer;">
            ${cap}(点击展开)
          </div>
        `);
        $elem.before($ph).hide().data('xdex-collapsed',true);
        $elem.addClass('xdex-generic-collapsed'); // ★ 标记为公用折叠,以免触发板块页长串折叠/收起
        $ph.on('click',()=>{
          if($elem.is(':visible')){
            $elem.hide(); $ph.html(`${cap}(点击展开)`);
          } else {
            $elem.show(); $ph.text('点击折叠');
          }
        });
      return $ph;
      },
      // ===== 引用串优化缓存相关 =====
      quoteCache: {},
      getQuoteFromCache(id) {
        return this.quoteCache[id] || GM_getValue('quote_' + id, null);
      },
      saveQuoteToCache(id, html) {
        this.quoteCache[id] = html;
        GM_setValue('quote_' + id, html);
      }
  };

  // XDex 门面:设置面板经 spUi/spData/spUpdate 调用,便于实现迁出 tag 0
  const XDex = {
    ui: Object.create(null),
    data: Object.create(null),
    update: Object.create(null)
  };
  function xdexCall(bucket, name, args) {
    const table = XDex[bucket];
    const fn = table && table[name];
    if (typeof fn !== 'function') {
      throw new Error('[XDex.' + bucket + '] missing ' + name);
    }
    return fn.apply(null, args);
  }
  function spUi(name) { return xdexCall('ui', name, Array.prototype.slice.call(arguments, 1)); }
  // 设置面板按钮 SVG 图标:分组添加(圆圈+加号)/ 保存(软盘)
  const XDEX_ICON_ADD_GROUP = '<svg class="xdex-icon-add-group" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 8v8M8 12h8"/></svg>';
  const XDEX_ICON_SAVE = '<svg class="xdex-icon-save" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8M7 3v5h8"/></svg>';
  const XDEX_ICON_EYE = '<svg class="xdex-icon-eye" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>';
  // 密码可见性:睁眼(密码隐藏态)/ 闭眼带斜线(密码显示态)
  const XDEX_ICON_EYE_OFF = '<svg class="xdex-icon-eye-off" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12z"/><circle cx="12" cy="12" r="3"/><line x1="4" y1="20" x2="20" y2="4"/></svg>';
  // 导入/导出:文件夹带箭头(export=向上、import=向下);重置=红色圆形箭头
  const XDEX_ICON_EXPORT_FILE = '<svg class="xdex-icon-export" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M12 16v-5M9.5 13.5L12 11l2.5 2.5"/></svg>';
  const XDEX_ICON_IMPORT_FILE = '<svg class="xdex-icon-import" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M12 11v5M9.5 13.5L12 16l2.5-2.5"/></svg>';
  const XDEX_ICON_RESET = '<svg class="xdex-icon-reset" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#EE0000" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 18A8.5 8.5 0 1 1 18.5 6.5"/><path d="M18.5 6.5l-1.3 3.6"/><path d="M19.2 10.6L18.5 6.5l-3.2 2.7"/></svg>';
  // WebDAV 连接检查(插头)/ 手动同步(双向循环箭头)
  const XDEX_ICON_PLUG = '<svg class="xdex-icon-plug" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 7V3M15 7V3"/><path d="M7 7h10v4a5 5 0 0 1-5 5 5 5 0 0 1-5-5z"/><path d="M12 16v5"/></svg>';
  const XDEX_ICON_SYNC = '<svg class="xdex-icon-sync" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 0 0-14.9-3"/><path d="M5.1 4v4h4"/><path d="M4 13a8 8 0 0 0 14.9 3"/><path d="M18.9 20v-4h-4"/></svg>';
  const XDEX_ICON_GEAR = '<svg class="xdex-icon-gear" viewBox="0 0 24 24" style="display:block;width:16px;height:16px;" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
  const XDEX_ICON_CHEVRON_DOWN = '<svg class="xdex-icon-chevron" viewBox="0 0 24 24" style="display:block;width:16px;height:16px;" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>';
  const XDEX_ICON_CHEVRON_LEFT = '<svg class="xdex-icon-chevron" viewBox="0 0 24 24" style="display:block;width:16px;height:16px;" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg>';
  // 展开/收起按钮视觉:当前展开→向下箭头;当前收起→向左箭头;图标用 currentColor 与常规图标同色
  function setPostExpandToggleVisual(btn, expanded) {
    if (!btn) return;
    btn.innerHTML = expanded ? XDEX_ICON_CHEVRON_DOWN : XDEX_ICON_CHEVRON_LEFT;
    btn.title = expanded ? '点击收起' : '点击展开';
  }
  // 导出串:折角文档 + 两条文字线(与页面内「导」按钮同一图形)
  const XDEX_EXPORT_ICON = '<svg class="xdex-icon-export" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.5 2.5H6.5A2 2 0 0 0 4.5 4.5v15a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7.5z"/><path d="M14.5 2.5v5h5"/><path d="M8.5 12h7M8.5 16.5h4.5"/></svg>';
  const XDEX_ICON_IMAGE = '<svg class="xdex-icon-image" viewBox="0 0 24 24" style="display:block;width:16px;height:16px;" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4"/><circle cx="9" cy="9" r="2"/><path d="M21 16l-4.6-4.6a2 2 0 0 0-2.8 0L5 20"/></svg>';
  function spData(name) { return xdexCall('data', name, Array.prototype.slice.call(arguments, 1)); }
  function spUpdate(name) { return xdexCall('update', name, Array.prototype.slice.call(arguments, 1)); }
  /* --------------------------------------------------
   * tag 1. 设置面板
   * -------------------------------------------------- */

  // 双刷新支持:如果上一次保存设置时要求执行第二次重载(localStorage 标记),
  // 则在页面加载时触发第二次重载并清理标记。
  try {
    const _flag = localStorage.getItem('myScriptSettings_doSecondReload');
    if (_flag === '1') {
      localStorage.removeItem('myScriptSettings_doSecondReload');
      console.log('[Settings] detected second-reload flag, performing second reload');
      // 延迟短暂时间以让页面先完成初始化任务,再执行第二次重载
      setTimeout(() => { try { location.reload(); } catch (e) { console.warn(e); } }, 200);
    }
  } catch (e) { /* ignore */ }

  const SETTINGS_BUTTON_ICON_32 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IB2cksfwAAAAlwSFlzAAAuIwAALiMBeKU/dgAACkBJREFUeJy1l3twU3UWx+soPphZd3Udl3XYdZxdfMz6nF1n1BnHXR/soi5EQBaxFkQB0UHXVUFAiyWBIhZZoLUItAVKIfRBaXuT9JFnkyZNbto0zyb35p3cVx6l0IKKytlzb0stD/lHNzOfSXPzy/l+f+f8HqcFBf/HV6xxtsTP/uJDO25gqaoZHH3obj5UeXuW2nXTxWPCjS8U0A2y+5HHkCk/izAX2D6FjTbOZNP2vWwm6mIzcQ8rROws5+9iEx3v8vSeu86PRdFpiAZJIc/+dPHgrn9yKQvP8jSwOQHYfB4ZGn/Pjz3jvEk+Wv+3SQaOIcPi3+KzBlp2Qz0t+8MVhejVsjuQe8NrZDPEz0HHlgIu1vZXJt33LRUjwTRoBiJgho5eNWidnWAIWYGkSQin/JDmKckEF29+vL//86vCDbJrUHz6UVr2NFKOMOPceCUDzyPTkVnh1bJrGPrwrSzraWW4AGj79dA5oAU7ZQNf2AMe2g2WsAM6fN3QbtOBPURCSjQh0EmXv271QcOWWxTHSwpQsASBScz9UQMoOmvcyDO7mzfcru0/siaZcuSotAeMQTOE4k5IcmIZeGCyHMSFJFBsFHQ+Gxx394A/QgKbTUE0pk+ZnZW7jpm3/VkURLhJBjZfKQMPBNfOfU69c0WRsnuHPBBstgX8WognbMBmksCKM8ymse45NJGdWANpNONLeEFj14EvYgZOCDN8vG2hiSybv8Pwwfy6wQVrUbgCeQrXwbUXiCbnvlFAy1ZMRx4kl6+6upSQ34MsCQ0qC2PUcY6idMAke1GYRQMR4DIJFM+MfRbf81mJdCYO9mAvtJMaSLO+b+iI/l0tWXNzqWrjy2Wd62dVOV/7xeVnLVsxFalEKN1H7z+jNG5Zbe3bWcgmDPXpFAkMNwhMwgQcF4KMQAOPKZYyIBqQSEslkUwIEXC7tVgC+whFt7lae2sVOJmbkFnIfZc1EJGtuBrFixHnFweLX96vL33A6Kx5KRhsDLJJI8VmUAAXYZbzw8lsAgRJLCfVWuAHgctxWJ4oZiY6lo0sgxmzDHMRZbnXW9kqJ6pnovhU5PUfrb34wgHTkQX4g98jCxiq7t10rItlhDgG5WAYBc4MZSGfTUoiooEhgcLnYchgFnixNLwfM+aFVKrvHB9tjgt01R6Fqvp5jPcXjP0wMkddbJ+mKbZfP57+milhWc005Eb8sriUKL1FFEceTAZrl7vsdQOBkBsYPICGUfjMiSEYzfPAp51YkgBkeB+M5FL4jIMhNJhifTAQ7gFXb+3XubT5TD6pt2xrr5qB8WQKYu+vUGNji7x7BZp4CJlaEJZV34rMtr65+z78skSuqpmGg5/e1Vl1m0p95KMm5dZ37KR+mM2kIIclOHMiJ5k4iWInhBBkGRKGBT+M5tKQY9wQTLpg25H94CPrY/mUPjKUtuWTEdXCzarqRzHujO312//esLXjVRSfidxTEJtTXRCVVV+ztUUxGw2Iqb8PuVdeTsxAHvfbyq6LBpvXsEzfiJBywEg2jgbykomxbHBweigjGRLSJLj6tWCxagyZmOrRE4ytcZhzscOsO0eHTV0HTIf/gaLvHFcYnfi+Fflkcv2Ltqg2XYUZeAi5E8XnINItx8VbfsdFG1SxQNPZk7gLzmdhMkO4U9Jxy7lUwjacoNVP5eKqhblE+5doYuBUJgI5Pvy1mjxegqLzDu1q+rxV3i2W4JcFe2ThO5E5pa2bFotiOPsn0MBvULxIXtEumeODO6bS3oNrbKaqUcqjgiRthVM59ocsYPozWPt4nDw3MNA+FPFWv3OC6THlk6b4Sd7/1SjunJFc4mxHv+aAGG/zvoNvlbYpbpaCo/ivkbvwQZH4WaGqeVqu3n+TvFz1ihDaOSXkrv2T3a4qN/SYvFpT53dWaxt4XW3fn8RZncb0iyURDQjpAQj4dYDjzvX0diUSlLYaDbxwkvM0jeIiPT0kfO8MaHGZ259TbDI2bP6y7rcXb8FXxjPwpFxdc0tpZetSh61FgwG/7jTbYDJGqwGYuE8yIGbgdF4ALtkPJKmeGKOzmIftdmJt1H/44YjfwLudNthdsfW78m2d59BEUl7seET+kfVSA+sJ5RPLCPOTH9bqN5iMxFcXi2stFrD0GrHe/vES5KVtmGdD4MabcvJYk1XP2W0tH++rVLiL166FooVLobq86is0MIIUKood111g4E2i9drFhGP1a0TP/Dfq7C+o25vtBrPhgqB9fSZIx3xwStoNY/UfzTFYCgbCIdcFYzVaE+iPHeE+2yRP/LtwJby1aDlsLnlvtLSsw4kGqpE7C2QEvQ55ajmhWfEvYkC5jOieWUj0leCz4vJWNemwNOUngnabgXLp4BQXHUs9noojUn0zkMWb0GUlfhDv1IP20Jeeg/sOVJeUfFA2f34RzJ7zMpRsePvb7XvrD6N4G/KsaOCPyP0vEr5H1xDV++YRg4Xr1E0r20jlfwJR6ygdsILRbJSCHiNUYNMehXzSIy4qPANY6RzIxgehsXYnaLraQKMzgq6rHfrNGvCHuvwd1raWaLB9JeXRfeN3mSERNYwe1SjLlAc6PjzwRceDooGZyIvI/FVE08dvEKql27o79zvoXoYXYsCxceh36CQDh5ua4NDuUohh83Eaj+MRvH5DePWWl22Eisod0K5WAWnqhDgdwAsrAxzelAmeOhuOdtO4E86JWRNSHtD3mM64zEor3V3xGBoI34bi9yB3v05oH3lTpa7U+nTtUYb6hs/EpEDRwR5c1Raoa2gA+fr3oM+kQXEGaGzRSj/5EJYtWwZtjbXg7NZAKkGDgA2KCI8XGB31g9dvlzIlGsgyfmky/aajPWHDzkdEA4vE2Y9lITyH6N1TFE66RkVhnseZ4LUr5DNAOdqghWiCD95/Gw5WyGHHp8Xw0qLFsHjJUthf+SnEcQGeFxaJpyLQQ9qgy2IFr9cp3aKigWGxt8TJ9OuUtbSxXMxA5CrkehFxN4Rp9TKGj56VZsC6JwKyuO/tpgY4slcBhyrWwaLCJSCbVwjln60H0nBs3OjYWA6z4xywQ5fZKs3W7ycn7o8RPMD0uKZchiOVdrdy3SU9QTRqUvM4e0FsNsTmY9KsaHcXfLZpHaxauRw2Fq+CPeUfQ8jbjf0fc8G4cNQuHkQTO2IwQE4c2+KOMfd0gd6lptooHX+BeMy1bVo8PfCdFAiv2slBpSykKagsV8CCRUugeMNaoEI9l4wZq30aNIMG0JFaMGG7HvDZxxctdk0sNq4BHSjpXuiLmk9NiBvU6wo8/rrdPB4oggiu8EuCY2ZsxmYo3VQMQcpzWfHzDKRc0BRxwtGwHeopMzRQFhR1QH3EBV1xD0R43GHcoHnCgNu77w46Zh6RAmCdJBOXCZyO9EPIY7qiuIA3pbiA49jEUnho+dkI+LkYBLk4RPH/CC7LS2sGs/HfCQOk79ASBltuKQAesxfX/3wG0qFuSIWubIDnsXzYqIo9otjATl6gE2MyyTMs4371f1z0RWiHZ4pgAAAAAElFTkSuQmCC';
  const SETTINGS_BUTTON_ICON_64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAHVVJREFUeJztewl0G/W9LkmA5raldKX0Ao+errzS0ntvOa/tLfRyeb20r9DYgUIuSUggkA0SAiSQFEISIie2yeJ4i7d4je14X0eWN1m7ZHmXJUuWNNo1iySvCSkJTfJ7v/9IdqR4ycI9r+edc+ec72hGM575f99vHyW33fbf239v/99urpoVS/7ea7jpzd+fsYQZTl/mM6bdETBm3DIBuib+cXt1fC9dHb/GURN/+3/lGv9LN0tP8hLWkvIgZy96kXM1Slh3i4P1Sj/hvLJPOU/nGOduMXJuSsK7GpJZW8lz/Gjmv4Rs6Xcsdk8k/UMk34QAxEVEPX733f9HlG5scxuOLWPNKfdy1pOPs562DpY1TLMhBtjxMcT4VYyFgA1xwAacwHIj51mfjubtp0Qhe/pPnOaMOfdFol9AwhmI6YgABH/D7x/9O9CcuwVGU28PWFN/zNvyPuacjS2sS9zNcqYL7Fgwlvi8QHGISExPgHPVVwdsGT+/9v7o+l9CwlURy88IoCPf/z34xmwBa9odnL04j3G1nmdYCzCcDdigHxjeAV7cd+J3TsYCLnYUvH48z9N43hf2gmvFCPHAemTneFvO20Fb+j9EPwfJfgEt/iQSVyKGEE/MnKuyx99RaY+/Dz9XIRJxf+3nJkbvjruD3h3/CP1e/I/wc8H45K3pTzM+/UWGNQOB362EXloGrbQGGu1aaDDJQaJrAaq/A7qUzdBpkIHCoYYhpxxcHh0wATfCAYx/EMWzojAB9Ia+i7w9f9eYPf0L1z4Pid+H+CmKsbzWEb8Myf4BkYtQI84jADFS5Yq79WqBpB+yvxe/E/EWYjcKsJXePbf8BO3pX2GdDSVk4X4k4GOGQWbvgiZ9O5zpU0FZnwaqVDJoaJNAQ3srNLZKoFrVBRUGFVSNqkA8imLZZOBwydEz7CiABfOCC0jocF75CG/P/VXAnr5svjVW2VcuRaJpiIEI8SsR8gQXquj4B2+JPBL+AWIv4p8EMfY8QwR5AkXIdO1ZOSsCYz7xFdZRs4Xl7eeJ9fy8E7T97VCllkGtvBMqjSqoo9UgcfRAl00HapMSlIMykJhQFHs3nDFooEYphdquDqhTdIDe3AUOEkLsSDg5Ek/wq31uW+mhkq6kBxOpg0uj11lLP78kYnVYAAerHStvvlQi8ScQK5DwrPvR78Utxe9Ujt3xs3HJ2Ip+w3o6u9igFxi0PO0fgRZtG1R0K6Gltx26R8VgdbbhOQOwPOYFzAksWtiNYg27VCB3aqHFqIQ6VUfYQzQYIhg2Ho7GcOjFfMAi/ODxyC509aSeONV5+NEkSvTl6LWilTcuIkAL4ua9AIn+b8RTjihrR75vQ1HuIvuJlOj7o9ZaOcPbLjG+Hly0DbQODVQY1aC1SsDhHQA/fs8GPfNn/jEsgSich7PDkA2FGJJB6YAOygc1ILepUCwMB9aE14YE8VwO8WX1QLatuCspD5/91VkB7HFfRpK+BQTwIt6+aQGQ5E/R5Xcivh/13fdRAAlaYBkuYEt62yEbw/SBP0AD7WgFs98IraMSMNFtQnZnOEyIeO76JTAMPzcKNUi8yNgLjZIW0AzVA8saw/kAz3N+/QWGrjnvHC290qD+WJUiSfj3mbUhyYwFBOAq6fi/3LwH7BbcfRXiL4g/IZ5HHGw9selHSH5fslh0smcou86Pbm82d4J5pBNcbgXY3BrBbUmZY3x9kcXfSB8QBol/BYpQq+mCaoUU+gYlmFQHwz1CwPkZ72l5Gxulxx0jqe/r+o9VlMqStiaKRaT8PYOYjpB2IjoRf8FyeG+t/9mb5i9s7t3xS9AD7kXijyF+WZX75s+QfB1i1+DgsSdZd7vMPloNSm0dWEaUSFgvZG4Cv7cnTD7oBk7oAmfcnl9cBDzvxb9TY2UgOaFZ0QYGazMQoUn+4LzSap7OfzixWbQsSSz6H7iWFMSxYsO6B5HsAST9AeJRzAvfqqY/Rwm8dsOHfBkhijzwXtaSvp1xtXmMw+Ug7WvF+JSD36MIk8AGhwl4wrGP4EK+qCaHtL6OhQUIefH8KDZNJmgdaoXyXjW0GyVg96jD9/IqGJwd4iJrWoL4n4jGJPHBTeWWF4gI37hlknT85m/Z4zdvQRxCPOaI33x75EE/RBxC7CDH43TG11hnY5vfXn3F4WgAj38IS5Ue/E6JkLiEzE3IkIaGlDJCKnoO4EfDLo3JjQs653oFJkeOxzLIGkA5IoWmdjHo+yWCAKQ8Wu2thqOS/IdEzQVCSUxsPEhy0nGSlxDf/DwCrELiEwhAtODxd5KbRLcdEYt24Y1PI+4n12F7+gjjaBjx2htwQYawVZl+FKAVS1dfuJHB+s37upEILRDkZkSZ6fvJNYLF/cAF7OF6H+MJKBA7CE6XGtSddaBUNQgVA4U+73F2fJYvzcsRUQUPJzQXLIkY6Z8RZxDP37IASDo9Qp7Aj/hXvOGj5fIk6eBgyvGANXMXay9NxrovZ9jhC2GLhFtfEuuMR0qmOqHmcyjE2ZATgjPNzLVWJj3BTG5AD+F4Y8RToj0hKNzPieE1YqgRcgrnkVTyrvpSTX/+uZOdpyQowg8jAixFPIOowPzwrVsV4GmEBRFEFObn7v0zuWEyJfr2hCPzbs6a9XtZf2n7gKnGzzprtayL8gm9Ox+JaeINpG4j+XNjPvjr5ASM8Zaw5YQQiAoD9IAgWjh8bjxc6wNW9AY6/F30kITEiaiMRwacm2rg6eKXeFv6keHhk4aM9nwxivDtGQ643v+FyEY8cCsC3IVuvw4/D/Rv2Easn4Y4MHMeH3QfQirtyT/BWXOP8t52tdDhhfjZBEcalwBWA0KeYCpgA35GAIFURAgsbRMBC4Q4YyRJhsUh4cBxpogQnquiobcwfhJm1EXsPkcCtvRs3paRc1qeV4treh/zQThfNQqeQBqlN0mluCVPiCj5W0Q64hGBvLhwOT7oY5G44AXGkvErjq7ScnTFWFgAfzgPCNZywyQufkaA8+M8hHjrlVkrj0XcHq8ZYwfg/EQQr7cCj6RnQ4IIhSHFovdwbA8KgiUw5BGe4/dqPmNs5V7OktrFj6aWGYazHsN11eD6no5a+9ORnPXDzyNAEuKpRIloScT6jyH5ZLLPWU4s451VH7OOxgkGuzfG1xu2FCl9aKXpMe+sAARngw6hKxSmO9wXiCICXkWUUBxMcoNIeADP+WJyARekgWN0gB4HvEeCpbAVWNspJmBNP8qZU7+Ja7sLUYkifCOy9i9FKsLmln36f0T8DnH3DZPHVvfr+MflMyUFif8AH3AQ8VtybOs/fqdpoGyXm26nNYpysOGUN2N9MsBMobWiBSAI+buBdXcJZZFDiwsJj3jAOBtzXViIIWDcUox5NVpcC7RLBgp5CRgHqoF1tVwc98onzgYt/KSvPZG3pn0xYqDtuM5nEcsjIpD+oFB8QBuH5Fsk+/TbJPt65p8K7fGFP6HjC3fS8QXfi/zxOsTWw1gCIzdfh8hGCK+eGHP6i+6RQoXbUibKydx7SSqtizRAHiFZjSPBawX4BImO+3sg4O4EnrS2JM45A4QYPYZBKEqAAIyTGYIMQgEvDLu7ocvQCvWNJ8E2VMDz7s4LY96OsSkMiwmfkuftBc9E1vgI4jjinyIc7iAhXHW05SSSX4FYh0LsFu/rju0OHXH5S5H4ayjA4/i5Af9oOSKLKDhzjYgqTMAbbxaJ82/DavAdvaqgtaP99MGB7vwHc9J3e/JOpYcTIXkFho1KAHuCawX46+S4IMIUZv4p7PSCDLbKWDY5Zy1MMlo8PyZcN425gXNJhVdqDJbONrscDpWdgoyswxB0y6Ymmb4rk0z3lSnecGU6YLk85m5q9Y+cuAstT8IgH7Fq1pObElbXJnWUI/lvovXvQQHScf+3MQJY/k8+NkCFcegFm/HzKSS+gmTRq+QL7sHYyk4QFwrWF2VQdyJKEMKUWFbw/oaivA99dpsmEgbo4h4ljPvUQoKbK8SMR3AYFnr4dGpKsLogECbIMa8avWIAQ2kAzKY22HsqC/YeSoTW5hzdpKd+17gj948B+nTmmKfzHBFzGqtMyNVo5a3ZvzjUUriCeMHM2sX7dU+VpzYoqo9I/g0F+DHiARQhGfFbFGL5rAiuuIJljviCB8yr8kkJ+RDxUZQA/0Hia/Y4g3oEUYgQbjCiO/qAQZOy32mTTJIcQOaAALp4yNMJ50LuBQUQSiS68V8nJ2ePz2HjNI0JL8AO41yhhr7uekhIPg4yuXjCPNy0GbP+HSF300sTfl3fdGD0s7NB22UiwLhPByFnbVVXb+Hjh8UFJWisO1s+7F6CRPfXH+6aaP5IrcD904ijiFJEDuJHc3IB1s3lQieFOWCWsLgQk1/hI1EC5CJi3raG7Bl34ZRWwrrbLrEe2YVwk2PA7E8vKsCM28ckS2YIE58OfPZ6/OwFJshc9rhUH9PGkvs5W8kjU5zBMsX2uMc94uMTPsUkIT/NW1E812eTfpmiQn1aTfIAWvhbSLITAYhPEcOICiIK4jnEvXMFwO4JUYx4khwfpAq+jAKUiepPCQ1FQqZ4GZKfRvzjtX/LmFMf5NziIf9IWoVnOPUyg3NCCDu3c9eUxEU9AgcgH42jr7MNfB7tFZ+r65LPPzLsMOQ8RJ4x7m5JHvdKx8ZctesDdMUH417F36axcZrxItJzDIw0T6MAryLBr6MIq06W5IiSGw7XkKQ4h/DMdireMiPAzxPFoqOIn0es/wu8WfrMdQmZ1E+QvF2U2TrvnM3bc1/3mjLbZeLDkyxdi5ax3DD5v06MYZXAidKPLS8mU4e1dQwHqytO85k3Ohr2LAmMpn0haMt6KUifSQzQleUYPpeneTMm0F4gn4Q8EcLt6JwgDVuUUf+AKEPcMy/53HjHA3nx9F7EF/Gix0kOSKISvisIQBXEowiimWuR/ApE2UJCBm0Z3+ZGM94V14qmhrRZlwOYDElW/2TcDxM41Ezj6DsfeZIsp/F8wNcjlD6PSwfm4UYwDYvBOlSYxI9m/Hrc0/LxNG+ynAvSn05ikiTihnPIOO6PYgiEPW3cr/HjuktEzfnh1rgq5YnEuiPFSWLRD+ZdNBL/aW48LUUh7hY6P0q0EwW4JyLAa5hQZt+nIfmNogzx/mvvwVnSlmKC+s3IYKlF2916RaHpgOqyw6CQJABVmwDi2kSg0a0DrHFe8hPYPJ3DChAkXSVjAnq0Ewx9Z6BdpQO5VgYOY8E5jPlPMMYvBh3VZt5aIEP3nzw/MTabTGcqzjTXf+5wS8EZFOBrwpoPaJsPHRNbEpsSfraQ4W6ren5qxl2eEYYISvS1iABvoAfsihJgBwogvBSh9cfJz2HfGDWcfn14oLoNiV8gC76KbpBpFNDT0wx6PQW8W3ZxGhsk4qpksaRCEMtNY9zPJMMQHjusEjAO1kOHSjt7ry7hPk1u61DRr93D4R9IUIgnJv1aHWmiJvxXhy/ibZQury2rrvzHCfv0O0WYBBNESufh9Nrr/4CKxP+E2I7V4KsRAV5HD3g3SoDtiHf40bQvuk25rxj6qxsU2q5PO9VqiCV/FeRcTw8FQwPiy36HCs6iCFM4+2PWRiECMd4wht3hiKEe+nvr5tyHCNKtFw+jCE+xlrTlvDV1+Zi7YdW5ED1NKkH0fUaHSvw1ZYXihP36ABEAMSXa3/2HGxGATFE7EF+PCPCqKDYEXsNE+BE9nP+WvoeyIvkLCxGPhlrXAUqdDKwWrdDsfCK8KxiPCoOQgAl05eHBBkJ03vtI1apLfb0NSquh6DmynpCj6L4Jbwc15lXOltWQzwjpKR9cSjmSfPFYovpKRIDLiHduRIAnEbsR90UE+CMKkBglwFNZxdW+ga7yv0W76PXQpZYL4dA3oJtNVtEg34WFYcBiqAAVCra4oO2f2YeLXkRPvNvSn/OolEqCjpYzkHJ0P6zf8Co8vWI1vP3mNjh5goKIAIieftFe7dKF2YcF+DV5AYLT4PcjAvxLAlWYHk/Rd8dT9oc2nulbvydf6unuPD0lVSlviDwJAY1OAgqtAoYNSmyO5naHZ7FShNvhCfDZKEDvuu59B/oaBlGE1cM9uc9v27YJ1qx5GVbEr4U/Ivk1f34Fdm97HbLTSv92VQD9ecT91xPgYUQq4hfk+IC49Cu7qMZSJJ+PSF7ZbN+57bQuWN1ebVCrWm9IgO5uCpOaGMdbI0ywpjntMRmQsK2dPZ7ESjAyLL2+sCrVFY283t9cVyRPOPgWvLLqVXh99SbYsXYL7Fm/Dfa9th0aio6YkbR1RoSEfT0HrifAvZH3AL+Pa6ZvR8vveZEy+N4Utz0WFQa5ZWVlWQPqM4xMLVt0kTKNHHyObsG1Z1z92hAY8yhjjqdwmLKYFNcVgKqnoDUt9UplyenPqDMi58ENO2DfK29C0sZ34I01m2H9CxsgO/29T5JOVr+TkNQ54wXjon3dVztCtOrDSHL2l97E5oO3I/mSj6ij7+C5XXju0FuUeDuGwo5nxfY74yjHV/eckv5bck5TjUV2kupWNV1cbJFaFQVjzt7ZeZ+Uv+gJkbj+tR4R8g2DYaBtwXu2KjTQ3NwKHTkpPllZtjg5u7HEpDlStOq59bDzpdfhnZe2CjngT3FrYO8Hb8CpjsTKw6XZ5Ui+P5ILfhctwElEHhLdgJ+/wc9/3US1lq+n1AN4vBWPv7aRUt2/iVIaV1L2Ivwum4TD5upea7vytNSoLBhfVIDOQhh3DsRk/OhxmAwxscnQA3ZlOXTrmue9n1iqgPr8YpDmp07oatLWHc2t340e+UrAlrbjhRfXQRzmgD+vXAd/+NOLEP/ntZCVtvtC6qlK66ETTYeR/GZEEAVIwVBYGhGA/iUSWo84gPsnEMdXU715b1I1g89Slnvw+Gd4LhFF0KMImbj/u/+kTM/kaST9MpN4YlRbclGh6Zp3saRKNJYnw7hrMEIu1tLngq6YcCD7nKEVZA0ZoNR2zrlfm1ID9UXFoKjOAmNXujqzsObJyGT66Bid/pOSU/vO79q5FTZveQ32f7gDyoqSoEed7U4qPqVIEKleRuLYFPUMIhoT9vXeHRHAMWdCSqIOLj1MJSRtoGQiJJyL+A+RuCAOUdCqOfm41lRh6nWqhBegPGcDQ0cZdKnmxqxaK4GEvTuAt2kEtz8bcggJb8bS01GvzaaYUdA3Z8HhD9+ChoaiWKu3d0FTZQV0nM4Dr7ETnB4V9DqkIO+rmmyQVlOW4RyhZE9zg5Pk/eInQoPFhxsiW8ulWsXx4+aB00+ODJT+u8eY/s/6ztIvHdyrmw2Bo2jl38c308LbnnixYwl+9721VPfxVyi5chVl+B6KtPQAVXLvsdbCd6n+crvRIQcOM3UgxEJgfAycvfXQJ62eI4C8qwze2LQJRnWV2KqGfyghb4eFfl2YEiN9PDsC3Y0n4c03NsPW1zeCTN05G+vlZVXQWZoLFp0Y/K5h4XkEfNADVq8eBuzUefVobafPkrlimhu+eG2YeR1qLL/tU0qtdEyhlYYsshw7Zz7xRFQOoHciMpB0MeIY7qchcJ8+8gGVVXmISnwQBfjqs5R1//tSlUY10niFC7iAx+QVCPlwMePA2PVgls/1AnFTLrz88gbQi/Ngwt0vkA+PrOgB44wwB4x7dGDvKoIP390Occ+thSMf78XQ0YC4Uw51BcWgqckB+6AMON4DgbGQ8LwZ8CE/eDkLmJztl5SWOgPOFFeuFcDv7IlZk0mSC4wpZXWMy8c1OEkofBHxXcR34ihaGCP3Uye2bKTaGldS1tNHlPLtvSPVMjczEn64IIA3vJixADCOHhhUx/bvNZXH4ZmVa6Ay7UPgzao5Lz4YkwyaCw/D2nUvC83LwYPvQiNVDfVl5dBekgXOoXbgx4IxpGOA5xjOBRZsr7XoIWfnGbV5T1/MmiwtGcCMpDwV5QFzcwDZVoodSzEZbttAdXlfpaRbBk3FG0fs4mku6I0IYL8qADlGCzn6arBXv9od1lYeE7Jx8gdvw4i84mr2Jz+IjKqgsfAQbHz1VeGaVWvWw5kzWSCurwZFRQ64DVK0MLcweQwDj9cMRpMaew0lunmnkFfmCtAfk5TtrSeAG019OLoMzpsDEG8hPt5LZZ7AhJgxYqnQuL09kRgkAlhxgd6YBbGeARhQVGLrqwo3Kg3pArnVa9dDc94hYQCaYixgVZVD8v63gZQtcp4gNWUfNBbngbIyG1ifRbDuQuR5FNCJpVXVrcRnaYRnabpbUVj/3PeLvqsCkJHa0ZpyJWBLFypA0Jb2m9sE8hRdiWQDCCPu2xAWxL6VEiv51xf/kNeekD1qb8UH87OL4Nh+jEuL4P6zi5sYB2dPBfTKwqGgUxXD758JE9z33nZwqXNAV58EWzdvnCVO8M47W6CjLhv6GrOAwZK5sNWRfNAPRmOXMFhFu7YO2+1rR+vw26He2WuU2g6gm06cZa3vL/GbTixjhk8ULJoDyDagTLjDYMg5TP7lZ/RCOK8cvcA5Z4GMYwCs8lJQqKQ40DRDVeEHQoIrTN8DR0Vvw5qXXo4hv2nzayA5cwiUNbnA+rGyjIcWsTyL5NVkJJ5TcfqwEs28HYrJNZwBOtTaSFcqRgFSteP2jCW20VNrXd1phvnCP2ajB4/90mwuVcdYenwCOE97pAxes1CMW/+oBgYVVYKVOhsOgqn6GJjrj8Pe3W/AyufXzpJ/EUOjNG0P9DZlInnrbHjNb3mP8PvjfOQJhvpr5u02zwZtOIqH85JO2gROaUqGw5r3U6mV6jUMFZkXJZ997NXbjIbs3Q5azMRmXx54X9eCC+YYGuyqMiHmqOpDkC7aBflH90BbxQHIT90tZH0iwIG/bIe+phScEocWJU+8wunQQ3fv/B0ngXGwat6XrZ+MeUGuCQ9suo4G8Pac2Nlrr86psyk+7TVXtC0qgLk/9X7zSJnWy5hireHXQgAtsliskrJoVhRASXEKPB2/WiD8n5jpEz/aAS1V+0F04E2g+4qBZWyL3mcm6ZnMHaDSty8ogHmoCj6dmpwrAg5hmm7yd1rQK2qg11rJF9v7oMkmhQFbRcKiAhiHsh+3060Mw7uirB8A3ttx3UUHcNFuTT401GTGZPs/xq2GDz/YBkOadGC9fdex/NUKY/dgrTe1gLRbFgmD2LdRpsEFBED09lIg13WBfKgZSuw9UEL3Q6+96ZLDdnrz4h5gyt/r9g9ejilJLGbp4NzkNx9YnxH07WmweeuWWQGefeElqCo7gn3D6A2Sv+oFBrcOKke1IMYqIMVxWdMtAZWuHWNcAQO9dejurDBrkH6ANFpjzAC4sHRLza1Qa1FBkb0f8u2DoMc5xufpcrPOhqcWJD+sPrTcPFrr4QLuWOuzfTe8aAI3uu6RQ7sF4hs3b4HqyhzgAouHz8JexUKLXQWn6CHIow2QTw/CaVsPVNlUUGeTQ4NVBrX4WWVTI9m+2evIZwkeSxzd4CReR/7lmVd92mfJ+/GCAlj6jv/MQrddicn+2PuTf8dzM4tmPCNAnTkCm7H2l5RkgtdnvjXyEZhcMmi0K6Hc3o3EDALBhUDOF9t7oYHWg8KpB7c/snbyL9Q9KpHHlPntecnLWt6/vc9YeNjju8ba6Pp8wHGTVgtgi9wEjeXHwcfcWOgsfC8vVh8VitiNk2Av6B0ykNk7QGLtgGZrJzRau4CySqHV1gUKWoWujrnBZwAv7xBCaDacgu7PGLfsRUt/7vw/lroNxx/vM5Xq5/TiZAAiv93fzKJxgvP0NYJLW/z5yJMRmNFHPofQEDOJeUJ4BulSFx2covMJT3t9topfzUt+tDvpTr2pLMtil5yd88c4zgYC1y9bMQ8LsuA1UeAxNn8+8tjRkflDuCd6IY9j8K3ej2MG5S7D8fnd32LIfEg/Ujns8A/N44I4//PGGyePscZgvLr7ysGlL4HF2tzFRUTX581C7M6S4Mzzd6I3AL+jMbFXJpr/BxKtsWzFsI2a9nH2edwZEyLbe4OuHwDPMAUuTTEOQXn4iYOOQ3VriyahJ+SeqLKJ4clxI7cgphu8Izmz/3vi/wLKEJjy325E2wAAAABJRU5ErkJggg==';
  function installSettingPanelEasterEgg(root = document) {
    const scope = root && root.querySelector ? root : document;
    const title = scope.querySelector('.xdex-setting-title-easter-egg');
    if (!title || title.__xdexSettingEasterEggBound) return;
    title.__xdexSettingEasterEggBound = true;
    const CLICK_WINDOW_MS = 3000;
    const REQUIRED_CLICKS = 3;
    const EASTER_EGG_TID = '68811442';
    let clicks = [];
    title.addEventListener('click', () => {
      const now = Date.now();
      clicks = clicks.filter(t => now - t <= CLICK_WINDOW_MS);
      clicks.push(now);
      if (clicks.length < REQUIRED_CLICKS) return;
      clicks = [];
      try {
        if (typeof enableQuotePreview === 'function') enableQuotePreview();
        if (typeof window.__xdexOpenQuoteByTid === 'function') {
          window.__xdexOpenQuoteByTid(EASTER_EGG_TID, { fromPOImage: true });
        }
      } catch (e) {
        console.warn('[X岛-EX] setting panel easter egg failed', e);
      }
    });
  }
  const SettingPanel = {
    key: 'myScriptSettings',
    defaults: {
      enableCookieSwitch: true,
      enableCookieConfirm: false, // 发送前确认饼干
      disableWatermark: true,
      // note: `duplicatePagination` 已弃用,使用 `enablePaginationDuplication`
      enablePaginationDuplication: true,
      updatePreviewCookie: true,
      hideEmptyTitleEmail: true,
      enableExternalImagePreview: true,  // 外部图床显示
      enableAutoCookieRefresh: true,
      enableAutoCookieRefreshToast: false,
      interceptReplyFormUnvcode: true, // 拦截回复中间页--unvcode
      interceptReplyFormU200B: true,
      interceptReplyFormAutoCompress: true,
      enableSeamlessPaging: true,
      enableAutoSeamlessPaging: true,
      enableHDImageAndLayoutFix: true,               // 启用高清图片链接
      enableLinkBlank: true, // 串页新标签打开
      enableAutoUrlLinkify: true,
      enableQuotePreview: true, // 优化引用弹窗
      enableUpdateCheck: true,
      enableImageContextMenu: true,
      enableImageHideMode: true, // 图片隐藏/无图模式
      applyImageHideMode: 'default', // default | blur | noimage | tips
      imageViewerSeparators: true, // 阅图模式:显示页码分隔线与“第N页”标识
      enableDraft: true,
      enableThreadHistory: true,   // 浏览历史:关闭后不再自动记录(已有数据保留,侧边栏入口隐藏)
      enablePostHistory: true,     // 发言历史:关闭后不再在发送消息后自动记录(手动添加不受影响)
      timeDisplayMode: 'relative', // relative | exact
      extendQuote: true, // 拓展引用格式
      extendQuoteAvailabilityDetection: true, // 检测引用串是否存在
      enablePostExpandAll: true, // 默认展开板块页长串
      kaomojiSort: 'default', // 颜文字排序:default | freq | recent
      toggleSidebar: false, // 侧边栏收起功能
      enableCustomDarkTheme: 'light', // 自定义深色模式:light=浅色 / dark=深色
      dockDisplayMode: 'fixed', // 扩展坞增强:hover=隐藏(悬浮显示)| fixed=固定显示(默认)
      postAfterAction: 'jump', // 发串后:jump=新标签页打开 / refresh=刷新页面回板块第一页
      disableAutoQuote: true, // 关闭引用:阻止URL中?r=参数自动插入引用号
      threadCookieWhitelistGroups: [],
      threadCookieWhitelistDisplayMode: 'fold', // 只看饼干:fold | hide | column
      poAnnotationSideDisplayMode: 'collapse', // 分栏侧栏:collapse | expand
      replyModeDefault: '回复',   // 板块页默认模式:发串/回复
      replyExtraDefault: '临时',  // 板块/时间线默认额外模式:临时/连续
      markedGroups: [],
      blockedCookies: [],
      blockedKeywords: [],
      favoriteThreads: [],
      subscriptionFeeds: [],    // [{desc, uuid}, ...]
      // 默认订阅号:'' = 未设置(点击“订阅EX”时每次弹窗选择)
      // updatedAt 用于 WebDAV / 导入导出按修改时间戳合并(新者胜;从未设置过则任一方有值即覆盖)
      defaultSubscriptionFeedUuid: '',
      defaultSubscriptionFeedUpdatedAt: 0,
      // 分组顺序元数据:{ [分组字段名]: { order: [语义键...], at: 时间戳 } }
      // 用于把「拖拽排序」作为可传播设置随 WebDAV/导入导出同步(静默合并,不触发冲突弹窗)
      groupOrderMeta: {},
      blockDisplayMode: 'hide'  // fold = 折叠 | hide = 隐藏
    },
    state: {},
    // JSONC 解析(支持 // 注释和尾随逗号)
    parseJSONC(str) {
      const cleaned = str
        .replace(/\/\/.*$/gm, '')
        .replace(/,(\s*[}\]])/g, '$1');
      return JSON.parse(cleaned);
    },
    // 导出为 JSONC 字符串
    buildJSONC(state) {
      const lines = [
        '// X岛-EX 配置文件',
        '// 支持 // 注释和尾随逗号',
        '{',
        '  "_meta": {',
        `    "version": "${typeof VERSION !== 'undefined' ? VERSION : 'unknown'}",`,
        `    "exportedAt": "${new Date().toISOString()}",`,
        '    "source": "nmbxd-EX"',
        '  },',
        '  "settings": {',
      ];
      const entries = Object.entries(state);
      entries.forEach(([key, val], i) => {
        const json = JSON.stringify(val, null, 2);
        if (json.includes('\n')) {
          const indented = json.split('\n').map((line, li) => li === 0 ? line : '    ' + line).join('\n');
          lines.push(`    ${JSON.stringify(key)}: ${indented}${i < entries.length - 1 ? ',' : ''}`);
        } else {
          lines.push(`    ${JSON.stringify(key)}: ${json}${i < entries.length - 1 ? ',' : ''}`);
        }
      });
      lines.push('  },');
      lines.push('}');
      return lines.join('\n');
    },
    // 校验导入的配置
    validateImport(incoming) {
      if (typeof incoming !== 'object' || incoming === null) return { valid: false, error: '配置内容无效' };
      const defaults = this.defaults;
      const validated = {};
      let skipped = 0;
      for (const [key, val] of Object.entries(incoming)) {
        if (!(key in defaults)) continue;
        if (key === 'blockedKeywords') {
          if (typeof val !== 'string' && !Array.isArray(val)) { skipped++; continue; }
          validated[key] = spData('normalizeBlockedKeywordGroups', val);
          continue;
        }
        if (typeof val !== typeof defaults[key]) { skipped++; continue; }
        if (Array.isArray(defaults[key]) && !Array.isArray(val)) { skipped++; continue; }
        validated[key] = val;
      }
      return { valid: true, merged: Object.assign({}, defaults, validated), skipped };
    },
    // 从文本导入(校验 + 暂存)
    importFromText(text) {
      let parsed;
      try {
        parsed = this.parseJSONC(text);
      } catch (e) {
        toast('配置文件格式错误'); return;
      }
      const incoming = parsed.settings || parsed;
      const result = this.validateImport(incoming);
      if (!result.valid) { toast(result.error); return; }
      this.__pendingImport = result.merged;
      const msg = result.skipped > 0
        ? `格式正确(${result.skipped} 个字段已跳过),请点击[应用]`
        : '格式正确,请点击[应用]';
      toast(msg);
      const btn = document.getElementById('btn_xdex_import_export');
      if (btn) btn.classList.remove('xdex-inv');
    },
    // 导出到剪贴板
    async exportToClipboard() {
      try {
        const jsonc = this.buildJSONC(this.state);
        await navigator.clipboard.writeText(jsonc);
        toast('配置已复制到剪贴板');
      } catch (e) {
        toast('复制失败,请重试');
      }
    },
    // 导出为文件
    exportToFile() {
      const jsonc = this.buildJSONC(this.state);
      const blob = new Blob([jsonc], { type: 'application/json' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = `x岛-ex-settings-v${typeof VERSION !== 'undefined' ? VERSION : 'unknown'}.jsonc`;
      a.click();
      URL.revokeObjectURL(a.href);
      toast('配置已导出');
    },
    // 从剪贴板导入
    async importFromClipboard() {
      try {
        const text = await navigator.clipboard.readText();
        if (!text) { toast('剪贴板为空'); return; }
        this.importFromText(text);
      } catch (e) {
        toast('无法读取剪贴板,请先点击页面后再试');
      }
    },
    // 从文件导入
    importFromFile() {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.json,.jsonc,.txt';
      input.onchange = () => {
        if (!input.files || !input.files[0]) return;
        const reader = new FileReader();
        reader.onload = () => this.importFromText(reader.result);
        reader.readAsText(input.files[0]);
      };
      input.click();
    },
    syncAuxiliaryControls() {
      const whitelistModeSelect = document.getElementById('sp_threadCookieWhitelistDisplayMode');
      if (whitelistModeSelect) {
        whitelistModeSelect.value = (this.state && this.state.threadCookieWhitelistDisplayMode) || 'fold';
      }
      const poSideModeSelect = document.getElementById('sp_poAnnotationSideDisplayMode');
      if (poSideModeSelect) {
        poSideModeSelect.value = (this.state && this.state.poAnnotationSideDisplayMode) || 'collapse';
      }
      const draftSelect = document.getElementById('sp_enableDraftMode');
      if (draftSelect) {
        const enabled = !!(this.state && this.state.enableDraft);
        draftSelect.value = enabled ? 'off' : 'on';
      }
      const expandSelect = document.getElementById('sp_postExpandAllMode');
      if (expandSelect) {
        const enabled = !!(this.state && this.state.enablePostExpandAll);
        expandSelect.value = enabled ? 'expand' : 'collapse';
      }
      const timeDisplaySelect = document.getElementById('sp_timeDisplayMode');
      if (timeDisplaySelect) {
        timeDisplaySelect.value = (this.state && this.state.timeDisplayMode === 'exact') ? 'exact' : 'relative';
      }
      const postAfterSelect = document.getElementById('sp_postAfterAction');
      if (postAfterSelect) {
        postAfterSelect.value = (this.state && this.state.postAfterAction === 'refresh') ? 'refresh' : 'jump';
      }
      // 快速回复默认模式(跨页/重开面板回填)
      const replyModeSelect = document.getElementById('sp_replyModeDefault');
      if (replyModeSelect) {
        replyModeSelect.value = (this.state && this.state.replyModeDefault === '发串') ? '发串' : '回复';
      }
      const replyExtraSelect = document.getElementById('sp_replyExtraDefault');
      if (replyExtraSelect) {
        replyExtraSelect.value = (this.state && this.state.replyExtraDefault === '连续') ? '连续' : '临时';
      }
    },
    init() {
      const saved = GM_getValue(this.key, {});
      const isFirstInit = Object.keys(saved).length === 0; // 判断是否首次初始化
      // console.log('init读取的原始数据:', JSON.stringify(saved));
          this.state = Object.assign({}, this.defaults, saved);
          if (this.state.timeDisplayMode !== 'exact') this.state.timeDisplayMode = 'relative';
      // 该功能为固定启用项:避免历史配置把它保存为 false 导致下拉无法生效
      this.state.enableImageHideMode = true;
      // console.log('init合并后的state:', JSON.stringify(this.state));
      // 兼容迁移:屏蔽饼干到组结构
      this.state.markedGroups = spData('normalizeMarkedGroups', this.state.markedGroups);
      this.state.blockedCookies = spData('normalizeBlockedGroups', this.state.blockedCookies);
      this.state.blockedKeywords = spData('normalizeBlockedKeywordGroups', this.state.blockedKeywords);
      this.state.favoriteThreads = spData('normalizeFavoriteThreads', this.state.favoriteThreads);
      this.state.threadCookieWhitelistGroups = spData('normalizeThreadCookieWhitelistGroups', this.state.threadCookieWhitelistGroups);
      // 清理废弃字段
      const validKeys = Object.keys(this.defaults);
      let needCleanup = false;
      Object.keys(this.state).forEach(key => {
        if (!validKeys.includes(key)) {
          delete this.state[key];
          needCleanup = true;
        }
      });
      // console.log('init清理后的state:', JSON.stringify(this.state));
      // 只在首次初始化或需要清理废弃字段时才保存
      if (isFirstInit || needCleanup) {
        console.log('首次初始化或需要清理,执行保存');
        GM_setValue(this.key, this.state);
      }
      this.render();
      GM_addValueChangeListener(this.key,(k,ov,nv,remote)=>{
        if(remote){
          this.state = Object.assign({}, this.defaults, nv);
          this.state.markedGroups = spData('normalizeMarkedGroups', this.state.markedGroups);
          this.state.blockedCookies = spData('normalizeBlockedGroups', this.state.blockedCookies);
          this.state.blockedKeywords = spData('normalizeBlockedKeywordGroups', this.state.blockedKeywords);
          this.state.favoriteThreads = spData('normalizeFavoriteThreads', this.state.favoriteThreads);
          this.state.threadCookieWhitelistGroups = spData('normalizeThreadCookieWhitelistGroups', this.state.threadCookieWhitelistGroups);
          if (this.state.timeDisplayMode !== 'exact') this.state.timeDisplayMode = 'relative';
          this.syncInputs();
          this.syncAuxiliaryControls();
          // 面板渲染后回填 WebDAV 已保存配置(tag29),避免需先点击输入框才显示
          try { if (typeof ensureWebdavConfigInPanel === 'function') ensureWebdavConfigInPanel(); } catch (e) {}
          try { renderFavoriteThreadsMenu(); } catch (e) {}
          try { refreshFilterDisplay(this.state); } catch (e) {}
          try { if (typeof window.__xdexApplyTimeDisplayMode === 'function') window.__xdexApplyTimeDisplayMode(document); } catch (e) {}
          try { if (typeof applyImageViewerSeparatorSetting === 'function') applyImageViewerSeparatorSetting(); } catch (e) {}
          // 引用号拓展(跨页同步):开启时对当前页重跑一次(幂等)
          try { if (this.state.extendQuote !== false && typeof extendQuote === 'function') extendQuote(document); } catch (e) {}
          // 发送前确认(跨页同步):按最新 state 运行时注入/清理串内检查区
          try {
            if (this.state.enableCookieSwitch && this.state.enableCookieConfirm) {
              const _tid = typeof PageType !== 'undefined' && PageType.getThreadId ? PageType.getThreadId(false) : '';
              const _short = _tid ? String(_tid).slice(0, 8) : '';
              if (_short) {
                if (typeof initThreadCookiePref === 'function') initThreadCookiePref(_short);
                if (typeof injectCookieCheckSwitch === 'function') injectCookieCheckSwitch(_short);
              }
            } else if (typeof clearCookieCheckSwitch === 'function') {
              clearCookieCheckSwitch();
            }
          } catch (e) {}
        }
      });
    },
    render() {
      if (!$('#xdex-setting-style').length) {
          $('head').append(`
              <style id="xdex-setting-style">
                  :root {
                      --xdex-sp-panel-bg: #FFFFEE;
                      --xdex-sp-fold-bg: #F0E0D6;
                      --xdex-sp-footer-bg: #FFFFEE;
                      --xdex-sp-border: #eee;
                      --xdex-sp-shadow: rgba(0,0,0,0.2);
                  }
                  :root.xdex-darkreader-active {
                      --xdex-sp-panel-bg: #2b2c2d;
                      --xdex-sp-fold-bg: #483327;
                      --xdex-sp-footer-bg: #2b2c2d;
                      --xdex-sp-border: #4b4d50;
                      --xdex-sp-shadow: rgba(0,0,0,0.55);
                      /* shadcn 风格 token:脚本内从未定义,导致 42 处 var() 恒取 fallback。
                         深色下面板底色已是 #2b2c2d,而 fallback 的 #333/#666 深字压深底不可读;
                         仅在深色类下补定义,light 模式继续走原 fallback(行为零变化) */
                      --foreground: #d9d0d0;
                      --muted-foreground: #9a9a9a;
                  }
                  .xdex-inv {opacity:0;pointer-events:none;}
                  #sp_panel {
                      background: var(--xdex-sp-panel-bg) !important;
                      box-shadow: 0 2px 10px var(--xdex-sp-shadow) !important;
                  }
                  .sp_fold,
                  .sp_fold_head,
                  .sp_fold_body {
                      background: var(--xdex-sp-fold-bg) !important;
                      border-color: var(--xdex-sp-border) !important;
                  }
                  #sp_panel_footer {
                      background: var(--xdex-sp-footer-bg) !important;
                      border-top-color: var(--xdex-sp-border) !important;
                  }
                  /* 设置按钮样式:图标入口 */
                  #sp_btn {
                      position:fixed;
                      top:10px;
                      right:10px;
                      z-index:10000;
                      width:36px;
                      height:36px;
                      padding:2px;
                      border:none;
                      background:rgba(102,204,255,.92);
                      border-radius:50%;
                      display:flex;
                      align-items:center;
                      justify-content:center;
                      overflow:visible;
                      box-shadow:0 1px 4px rgba(0,0,0,.18);
                      transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
                      cursor:pointer;
                  }
                  #sp_btn:hover {
                      background:#66CCFF;
                      box-shadow:0 2px 8px rgba(0,0,0,.24);
                      transform:translateY(-1px);
                  }
                  #sp_btn img {
                      display:block;
                      width:32px;
                      height:32px;
                      pointer-events:none;
                      user-select:none;
                  }
                  #sp_btn.xdex-has-update::after {
                       content:'!';
                       position:absolute;
                       right:-6px;
                      top:-6px;
                      width:18px;
                      height:18px;
                      border-radius:50%;
                      background:#e53935;
                      color:#fff;
                      font-size:12px;
                      line-height:18px;
                      text-align:center;
                      font-weight:bold;
                      box-shadow:0 1px 4px rgba(0,0,0,.25);
                  }
                  #sp_panel_footer .sp_panel_links.xdex-update-highlight {
                       box-shadow: inset 0 0 0 2px #ff4d4f, 0 0 0 2px rgba(255,77,79,.18);
                       background: linear-gradient(90deg, rgba(255,77,79,.08), rgba(255,193,7,.08)) !important;
                       transition: box-shadow 0.2s ease, background 0.2s ease, filter 0.2s ease;
                  }
                  #sp_panel_footer .sp_panel_links.xdex-update-source-greasyfork {
                       box-shadow: inset 0 0 0 2px rgba(255,152,0,.68), 0 0 0 2px rgba(255,152,0,.16);
                  }
                  #sp_panel_footer .sp_panel_links.xdex-update-source-scriptcat {
                       box-shadow: inset 0 0 0 2px rgba(18,150,219,.68), 0 0 0 2px rgba(18,150,219,.16);
                  }
                  #sp_panel_footer .sp_panel_links a.xdex-update-link-primary {
                       color:#c62828 !important;
                       font-weight:700;
                       text-decoration:underline;
                       text-underline-offset:2px;
                  }
                  #sp_panel_footer .sp_panel_links a.xdex-update-link-secondary {
                       color:#8a1f1f !important;
                       font-weight:600;
                  }
                  #sp_panel_tab_slot {
                         position:absolute;
                         left:-112px;
                         top:48px;
                         width:112px;
                         display:flex;
                        flex-direction:column;
                         align-items:flex-end;
                         gap:6px;
                    }
                  #sp_panel_tab_slot:empty {
                        display:none;
                  }
                  #sp_panel_tab_slot .sp_panel_tab {
                         --sp-panel-tab-bg:#006666;
                         --sp-panel-tab-icon-size:28px;
                         --sp-panel-tab-label-width:4em;
                         --sp-panel-tab-collapsed-width:42px;
                         --sp-panel-tab-expanded-width:calc(var(--sp-panel-tab-icon-size) + var(--sp-panel-tab-label-width) + 20px);
                         width:var(--sp-panel-tab-collapsed-width);
                         min-height:50px;
                         padding:6px;
                         border:1px solid var(--xdex-sp-border);
                         border-right:none;
                        border-radius:999px 0 0 999px;
                        background:var(--sp-panel-tab-bg);
                        color:#fff;
                        cursor:pointer;
                         display:flex;
                         align-items:center;
                         justify-content:flex-start;
                         gap:8px;
                         overflow:hidden;
                         box-sizing:border-box;
                         transition:width .16s ease, filter .16s ease;
                    }
                  #sp_panel_tab_slot .sp_panel_tab[data-sp-module="settings"] {
                         --sp-panel-tab-bg:#006666;
                    }
                  #sp_panel_tab_slot .sp_panel_tab[data-sp-module="history"] {
                          --sp-panel-tab-bg:#0080FF;
                     }
                  #sp_panel_tab_slot .sp_panel_tab[data-sp-module="posts"] {
                          --sp-panel-tab-bg:#FFFF00;
                          color:#332200;
                     }
                  #sp_panel_tab_slot .sp_panel_tab[data-sp-module="feeds"] {
                          --sp-panel-tab-bg:#39C5BB;
                          color:#440022;
                     }
                  #sp_panel_tab_slot .sp_panel_tab.active,
                  #sp_panel_tab_slot .sp_panel_tab:hover,
                  #sp_panel_tab_slot .sp_panel_tab:focus,
                  #sp_panel_tab_slot .sp_panel_tab:focus-visible,
                  #sp_panel_tab_slot .sp_panel_tab.is-hover {
                         width:var(--sp-panel-tab-expanded-width);
                         font-weight:bold;
                    }
                  #sp_panel_tab_slot .sp_panel_tab_icon {
                         width:var(--sp-panel-tab-icon-size);
                         height:var(--sp-panel-tab-icon-size);
                         line-height:var(--sp-panel-tab-icon-size);
                         border-radius:50%;
                         background:rgba(255,255,255,.18);
                         flex:0 0 var(--sp-panel-tab-icon-size);
                         text-align:center;
                       display:flex;
                       align-items:center;
                       justify-content:center;
                    }
                  #sp_panel_tab_slot .sp_panel_tab_icon svg {
                         width:72%;
                         height:72%;
                         display:block;
                         margin:auto;
                    }
                  #sp_panel_tab_slot .sp_panel_tab_label {
                         width:var(--sp-panel-tab-label-width);
                         flex:0 0 var(--sp-panel-tab-label-width);
                         text-align:center;
                         white-space:nowrap;
                         opacity:0;
                         transition:opacity .16s ease;
                    }
                  #sp_panel_tab_slot .sp_panel_tab.active .sp_panel_tab_label,
                  #sp_panel_tab_slot .sp_panel_tab:hover .sp_panel_tab_label,
                  #sp_panel_tab_slot .sp_panel_tab:focus .sp_panel_tab_label,
                  #sp_panel_tab_slot .sp_panel_tab:focus-visible .sp_panel_tab_label,
                  #sp_panel_tab_slot .sp_panel_tab.is-hover .sp_panel_tab_label {
                         opacity:1;
                    }
                  #sp_panel_views {
                        display:flex;
                        flex-direction:column;
                        flex:1;
                        min-height:0;
                   }
                  .sp_panel_module.active {
                        display:flex;
                        flex-direction:column;
                        flex:1;
                        min-height:0;
                  }
                  #sp_module_history.sp_panel_module.active,
                  #sp_module_posts.sp_panel_module.active,
                  #sp_module_feeds.sp_panel_module.active {
                         display:flex;
                         flex-direction:column;
                         flex:1;
                         min-height:0;
                  }
                  .sp_panel_module:not(.active) {
                         display:none;
                    }
                   .sp_panel_content {
                          padding:18px;
                          overflow-y:auto;
                          flex:1 1 auto;
                          min-height:300px;
                          box-sizing:border-box;
                     }
                   #sp_history_content {
                          display:block;
                     }
                  .xdex-history-toolbar {
                         display:flex;
                         align-items:center;
                         gap:8px;
                         margin:0 0 10px;
                    }
                  .xdex-history-toolbar input {
                          flex:1;
                          min-width:0;
                          padding:6px 8px;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:8px;
                          background:var(--xdex-sp-panel-bg);
                      }
                  .xdex-history-toolbar select {
                          padding:6px 8px;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:8px;
                          background:var(--xdex-sp-panel-bg);
                     }
                   .xdex-history-toolbar .xdex-history-count {
                           white-space:nowrap;
                      }
                   .xdex-history-toolbar .xdex-history-search-help {
                           display:inline-block;
                           margin-left:3px;
                           text-decoration:underline;
                           cursor:help;
                           color:inherit;
                      }
                   #sp_history_results {
                           display:block;
                           min-height:40px;
                           padding-top:8px;
                           color:inherit;
                      }
                   #sp_posts_results {
                           display:block;
                           min-height:40px;
                           padding-top:8px;
                           color:inherit;
                      }
                   .xdex-post-history-type-buttons {
                          display:flex;
                          gap:8px;
                          margin:0 0 10px;
                     }
                   .xdex-post-history-type-buttons button {
                          flex:1;
                          padding:6px 8px;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:8px;
                          background:var(--xdex-sp-panel-bg);
                          cursor:pointer;
                     }
                   .xdex-post-history-type-buttons button.active {
                           background:#F0E0D6;
                           color:#332200;
                           font-weight:bold;
                      }
                  .xdex-recycle-btn {
                          position:relative;
                          display:inline-flex;
                          align-items:center;
                          justify-content:center;
                          width:30px;
                          height:30px;
                          padding:0;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:8px;
                          background:var(--xdex-sp-panel-bg);
                          cursor:pointer;
                          color:inherit;
                     }
                  /* 通用图标按钮(分组添加/保存):与回收站按钮同款外观 */
                  .xdex-icon-btn {
                          display:inline-flex;
                          align-items:center;
                          justify-content:center;
                          width:30px;
                          height:30px;
                          padding:0;
                          border:1px solid var(--xdex-sp-border, #bfa58f);
                          border-radius:8px;
                          background:var(--xdex-sp-panel-bg, #F0E0D6);
                          cursor:pointer;
                          color:inherit;
                     }
                  :root.xdex-custom-dark .xdex-icon-btn,
                  :root.xdex-darkreader-active .xdex-icon-btn {
                          background:#2b2c2d;
                          border-color:#555;
                          color:#d9d0d0;
                     }
                  .xdex-icon-btn:hover {
                          border-color:#00ffcc;
                          color:#00ffcc;
                     }
                  /* 图标按钮专用隐藏类:占位透明(与 xdex-inv 布局语义一致,避免折叠/展开高度突变) */
                  .xdex-btn-hidden { opacity:0; pointer-events:none; }
                  .xdex-recycle-btn:hover {
                          border-color:#EE0000;
                          color:#EE0000;
                     }
                  .xdex-recycle-badge {
                          position:absolute;
                          top:-6px;
                          right:-6px;
                          min-width:16px;
                          height:16px;
                          padding:0 4px;
                          border-radius:999px;
                          background:#EE0000;
                          color:#fff;
                          font-size:10px;
                          line-height:16px;
                          text-align:center;
                          pointer-events:none;
                     }
                  .xdex-recycle-bar {
                          display:none;
                          align-items:center;
                          gap:8px;
                          margin:0 0 10px;
                          padding:6px 8px;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:8px;
                          background:var(--xdex-sp-fold-bg);
                     }
                  .xdex-recycle-bar .xdex-recycle-back {
                          padding:4px 10px;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:8px;
                          background:var(--xdex-sp-panel-bg);
                          cursor:pointer;
                     }
                  .xdex-recycle-bar .xdex-recycle-bar-title {
                          flex:1;
                          white-space:nowrap;
                          overflow:hidden;
                          text-overflow:ellipsis;
                     }
                  .xdex-recycle-bar .xdex-recycle-empty-btn {
                          padding:4px 10px;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:8px;
                          background:var(--xdex-sp-panel-bg);
                          cursor:pointer;
                     }
                  .xdex-recycle-bar .xdex-recycle-empty-btn:disabled {
                          opacity:.5;
                          cursor:not-allowed;
                     }
                  .xdex-recycle-mode .xdex-history-toolbar,
                  .xdex-recycle-mode .xdex-post-history-type-buttons {
                          display:none;
                     }
                  .xdex-recycle-mode .xdex-posts-add-row {
                          display:none !important;
                     }
                  .xdex-recycle-mode .xdex-recycle-bar {
                          display:flex;
                     }
                  .xdex-recycle-empty {
                          margin:24px 0;
                          padding:20px 12px;
                          border:1px dashed var(--xdex-sp-border);
                          border-radius:8px;
                          text-align:center;
                          color:var(--foreground, #333);
                          opacity:.75;
                     }
                  .xdex-history-tombstone-mark,
                  .xdex-post-history-tombstone-mark {
                          position:absolute;
                          top:-9px;
                          right:34px;
                          width:20px;
                          height:20px;
                          border:1px solid #a98f7a;
                          border-radius:999px;
                          background:#F0E0D6;
                          display:flex;
                          align-items:center;
                          justify-content:center;
                          padding:0;
                          cursor:pointer;
                          z-index:1;
                          color:#5b4636;
                     }
                  .xdex-history-tombstone-mark:hover,
                  .xdex-post-history-tombstone-mark:hover {
                          color:#EE0000;
                          border-color:#EE0000;
                     }
                  .xdex-recycle-sort {
                          padding:4px 6px;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:6px;
                          background:var(--xdex-sp-panel-bg);
                          color:inherit;
                          cursor:pointer;
                     }
                  .xdex-recycle-item {
                          position:relative;
                          border:1px solid #bfa58f;
                          border-radius:8px;
                          padding:10px 12px;
                          margin-bottom:8px;
                          background:rgba(255,255,255,0.18);
                     }
                  .xdex-recycle-item-title {
                          font-weight:bold;
                          margin-bottom:4px;
                          word-break:break-all;
                     }
                  .xdex-recycle-item-meta {
                          font-size:12px;
                          opacity:.75;
                     }
                  .xdex-recycle-item.xdex-recycle-highlight {
                          animation:xdexRecycleHighlight 2s ease;
                     }
                  @keyframes xdexRecycleHighlight {
                          0%,60% { background:#ffe08a; }
                          100% { background:rgba(255,255,255,0.18); }
                     }
                  .xdex-recycle-item-meta {
                          display:flex;
                          align-items:center;
                          gap:6px;
                     }
                  .xdex-recycle-item-meta > span {
                          flex:1;
                          min-width:0;
                     }
                  .xdex-recycle-icon-btn,
                  .xdex-recycle-item-btn {
                          background:none;
                          border:none;
                          padding:2px;
                          cursor:pointer;
                          color:#6b5644;
                          display:inline-flex;
                          align-items:center;
                          justify-content:center;
                          border-radius:4px;
                     }
                  .xdex-recycle-icon-btn:hover { color:#EE0000; }
                  .xdex-recycle-item-restore:hover { color:#00ffcc; }
                  .xdex-recycle-item-purge:hover { color:#EE0000; }
                  .xdex-recycle-item-actions {
                          display:flex;
                          gap:6px;
                          margin-top:8px;
                          justify-content:flex-end;
                     }
                  .xdex-recycle-item-btn {
                          padding:3px 10px;
                          border:1px solid var(--xdex-sp-border);
                          border-radius:6px;
                          background:var(--xdex-sp-panel-bg);
                          color:inherit;
                          cursor:pointer;
                          font-size:12px;
                     }
                  .xdex-recycle-item-btn.xdex-recycle-item-restore:hover {
                          border-color:#00ffcc;
                          color:#00ffcc;
                     }
                  .xdex-recycle-item-btn.xdex-recycle-item-purge:hover {
                          border-color:#EE0000;
                          color:#EE0000;
                     }
                   .xdex-history-item {
                           display:block !important;
                            position:relative;
                            margin:10px 0 8px;
                            border:1px solid var(--xdex-sp-border);
                            border-radius:8px;
                            background:var(--xdex-sp-fold-bg);
                            overflow:visible;
                       }
                   .xdex-history-item .h-threads-item-main {
                          display:block !important;
                           position:relative;
                           padding:10px;
                           background:transparent;
                      }
                    .xdex-history-item .h-threads-content {
                           display:block !important;
                           margin:15px 40px 6px;
                           font-size:14px;
                            line-height:20px;
                           white-space:pre-wrap;
                           overflow-wrap:anywhere;
                      }
                    .xdex-history-item .h-threads-info {
                           display:flex;
                           align-items:flex-start;
                           gap:8px;
                           line-height:1.5;
                           margin-bottom:6px;
                      }
                    .xdex-history-info-main {
                           flex:1 1 auto;
                           min-width:0;
                      }
                   .xdex-history-item .h-threads-info-title {
                          color:#cc1105;
                          font-weight:bold;
                     }
                   .xdex-history-item .h-threads-info-email {
                          color:#117743;
                          font-weight:bold;
                     }
                    .xdex-history-item .h-threads-info-email,
                    .xdex-history-item .h-threads-info-createdat,
                    .xdex-history-item .h-threads-info-uid,
                    .xdex-history-item .h-threads-info-id,
                    .xdex-history-item .h-threads-info-reply-btn {
                            margin-left:5px;
                       }
                    .xdex-history-delete,
                    .xdex-post-history-delete,
                    .xdex-history-reply-action {
                           text-decoration:none;
                           border:0;
                           background:transparent;
                          padding:0;
                          font:inherit;
                           cursor:pointer;
                      }
                    .xdex-history-delete:hover,
                    .xdex-post-history-delete:hover,
                    .xdex-history-reply-action:hover {
                           text-decoration:underline;
                      }
                    .xdex-history-reply-action {
                           color:#07d;
                      }
                    .xdex-history-reply-action:hover {
                           color:#059;
                      }
                     .xdex-history-delete,
                     .xdex-post-history-delete {
                             position:absolute;
                             top:-9px;
                             right:10px;
                            width:20px;
                            height:20px;
                            border:1px solid #a98f7a;
                            border-radius:999px;
                            background:#F0E0D6;
                            line-height:16px;
                            padding:0;
                            font-size:14px;
                            z-index:1;
                            color:#800000;
                       }
                    /* 深色:历史卡片圆钮(删除/墓碑)与导入预览块走面板 token,浅色模式保持原样 */
                    :root.xdex-darkreader-active .xdex-history-delete,
                    :root.xdex-darkreader-active .xdex-post-history-delete,
                    :root.xdex-darkreader-active .xdex-history-tombstone-mark,
                    :root.xdex-darkreader-active .xdex-post-history-tombstone-mark {
                        background: var(--xdex-sp-fold-bg);
                        color: var(--foreground);
                    }
                    /* 该元素背景/边框写在内联 style 上,必须 !important 才能压过 */
                    :root.xdex-darkreader-active #sp_fullExport_import_preview {
                        background: var(--xdex-sp-panel-bg) !important;
                        border-color: var(--xdex-sp-border) !important;
                    }
                    .xdex-history-footer {
                           display:flex;
                           align-items:center;
                           flex-wrap:wrap;
                           gap:0;
                           clear:both;
                           margin-top:8px;
                           color:#777;
                           font-size:12px;
                      }
                    .xdex-history-footer span + span::before {
                           content:'·';
                           margin:0 6px;
                           color:#777;
                      }
                    .xdex-history-po-label {
                           color:#00FFCC;
                           font-weight:bold;
                      }
                  .xdex-history-header {
                         justify-content:space-between;
                         margin-bottom:6px;
                    }
                  .xdex-history-meta {
                         flex-wrap:wrap;
                         font-size:12px;
                    }
                  .xdex-history-image,
                  .xdex-post-history-image {
                          display:block;
                          width:112px;
                          max-height:112px;
                         margin:6px 10px 6px 0;
                         overflow:hidden;
                         float:left;
                    }
                  .xdex-history-image img,
                  .xdex-post-history-image img {
                          max-width:112px;
                          max-height:112px;
                          object-fit:contain;
                    }
                   .xdex-history-footer,
                   .xdex-history-empty {
                          clear:both;
                          margin-top:8px;
                         color:#777;
                         font-size:12px;
                  }
              /* 饼干 switch 动效 */
            .xdex-pin-btn {
              transition: background-color 0.2s ease, border-color 0.2s ease;
            }
            .xdex-pin-btn:checked {
              transition: background-color 0.2s ease, border-color 0.2s ease;
                  }
            </style>
          `);
      }
      if (!$('#sp_btn').length) {
        $('body').append(
            $(`<button id="sp_btn" type="button" title="X岛-EX 设置" aria-label="X岛-EX 设置"><img src="${SETTINGS_BUTTON_ICON_32}" srcset="${SETTINGS_BUTTON_ICON_32} 1x, ${SETTINGS_BUTTON_ICON_64} 2x" width="32" height="32" alt=""></button>`)
                .on('click',()=>{
                  this.syncInputs();
                  this.syncAuxiliaryControls();
                  if (typeof window.__xdexSyncDarkReaderTheme === 'function') window.__xdexSyncDarkReaderTheme();
                  $('#sp_cover').fadeIn();
                  spUpdate('maybeShowPendingUpdateDialogOnPanelOpen');
                })
        );
        spUpdate('updateSettingsButtonBadge', spUpdate('getUpdateCheckState'));
    }
      const fold = (id,title,ph) => `
        <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
          <div class="sp_fold_head" data-btn="#btn_${id}"
              style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
            <span>${title}</span>
            <button id="btn_${id}" class="sp_save xdex-inv" data-id="${id}"
                    style="margin-left:auto;padding:2px 8px;">保存</button>
          </div>
          <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
          <input id="${id}" style="width:100%;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="${ph}">
          </div>
        </div>`;
      const checkboxItemStyle = 'width:50%; display:flex; align-items:center; gap:8px;';
      const checkboxRowStyle = 'width:50%; display:flex; align-items:center; gap:8px;';
      const quickReplyRowStyle = 'display:flex; align-items:center; gap:12px; margin:4px 0;';
      const quickReplyColumnStyle = 'flex:1; display:flex; flex-direction:column; align-items:flex-start; gap:4px;';
      const html = `
        <style>
          .fixed-on {
            accent-color: #000; /* 勾选颜色黑色 */
          }
          .fixed-on:disabled {
            opacity: 1;         /* 不降低透明度 */
            filter: none;       /* 去掉可能的灰度滤镜 */
            cursor: default;    /* 鼠标变成普通箭头 */
          }
          .xdex-switch { appearance:none; width:34px; height:18px; flex:0 0 34px; border:1px solid #a98f7a; border-radius:999px; background:#EE0000; cursor:pointer; position:relative; box-sizing:border-box; transition:background .18s ease,border-color .18s ease; }
          .xdex-switch::before { content:""; position:absolute; width:14px; height:14px; left:1px; top:1px; border-radius:50%; background:#FFFFEE; box-shadow:0 1px 2px rgba(0,0,0,.24); transition:transform .18s ease; }
          .xdex-switch:checked { background:#66CCFF; border-color:#7da6bf; }
          input.xdex-switch.fixed-on:checked:disabled { background:#00FFCC; border-color:#00b894; }
          .xdex-switch:checked::before { transform:translateX(16px); }
          /* 饼干偏好开关圆圈:当前饼干命中本串偏好→深青(#006666),未命中→红(#EE0000);未开启偏好不着色 */
          .xdex-cookie-check-sw.xdex-cookie-pref-match::before { background:#006666; }
          .xdex-cookie-check-sw.xdex-cookie-pref-mismatch::before { background:#EE0000; }
          .xdex-switch:focus-visible { outline:2px solid #cc1105; outline-offset:2px; }
          .xdex-switch:disabled { opacity:1; cursor:default; }
        </style>
        <div id="sp_cover" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:9999;">
          <div id="sp_panel" style="
              position:relative;margin:40px auto;width:min(711px, calc(100vw - 32px));
              max-height:calc(100vh - 80px);background:#FFFFEE;border-radius:8px;
              display:flex;flex-direction:column;box-shadow:0 2px 10px rgba(0,0,0,0.2);">
            <div id="sp_panel_tab_slot" aria-label="设置面板模块">
              <button type="button" class="sp_panel_tab" data-sp-module="settings"><span class="sp_panel_tab_icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg></span><span class="sp_panel_tab_label">设置</span></button>
              <button type="button" class="sp_panel_tab" data-sp-module="history"><span class="sp_panel_tab_icon"><svg viewBox="-1 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v5h5"></path><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"></path><path d="M12 7v5l4 2"></path></svg></span><span class="sp_panel_tab_label">浏览历史</span></button>
              <button type="button" class="sp_panel_tab" data-sp-module="posts"><span class="sp_panel_tab_icon"><svg viewBox="-0.8 0.3 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg></span><span class="sp_panel_tab_label">我的发言</span></button>
              <button type="button" class="sp_panel_tab" data-sp-module="feeds"><span class="sp_panel_tab_icon"><svg viewBox="0 -0.5 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg></span><span class="sp_panel_tab_label">我的订阅</span></button>
            </div>
            <div id="sp_panel_views">
              <div id="sp_module_settings" class="sp_panel_module active" data-sp-module-view="settings">
                <div id="sp_panel_content" class="sp_panel_content" style="padding:18px;overflow-y:auto;flex:1;min-height:300px;box-sizing:border-box;">
                  <div id="sp_panel_title" style="margin:0 0 10px; position:relative; text-align:center;">
                <span class="xdex-setting-title-easter-egg" style="font-size:20px; font-weight:bold; cursor:pointer;">X岛-EX</span>
                <div style="position:absolute; right:0; top:50%; transform:translateY(-50%); display:flex; align-items:center; gap:10px;">
                <label for="sp_enableCustomDarkTheme" id="sp_dark_toggle_wrap" title="深色模式" style="display:inline-flex; align-items:center; cursor:pointer;">
                  <input type="checkbox" id="sp_enableCustomDarkTheme" role="switch" style="position:absolute;opacity:0;width:0;height:0;">
                  <span id="sp_dark_toggle_track" style="display:inline-flex;align-items:center;width:46px;height:24px;border-radius:12px;background:#fff;border:1px solid #ccc;transition:background .35s ease,border-color .35s ease;box-sizing:border-box;">
                    <span id="sp_dark_toggle_thumb" style="display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.25);margin-left:3px;transition:transform .35s ease,background .35s ease;">
                      <svg id="sp_dark_icon_sun" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#f39c12" stroke-width="2.4" stroke-linecap="round"><circle cx="12" cy="12" r="4.5" fill="#f39c12" stroke="none"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5 5l2.1 2.1M16.9 16.9L19 19M19 5l-2.1 2.1M7.1 16.9L5 19"/></svg>
                      <svg id="sp_dark_icon_moon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#ffd54f" stroke-width="2.2" stroke-linecap="round" style="display:none;"><path d="M20 14.5A8.5 8.5 0 0 1 9.5 4a8.5 8.5 0 1 0 10.5 10.5z" fill="#ffd54f" stroke="none"/></svg>
                    </span>
                  </span>
                </label>
                <a id="sp_version_link" href="javascript:void(0)" style="font-size:12px; color:#999; text-decoration:underline;">v2.1.0.1</a>
                </div>
              </div>
                  <div id="sp_checkbox_container" style="display:flex;flex-wrap:wrap;">
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableCookieSwitch" class="xdex-switch" role="switch"><label for="sp_enableCookieSwitch"> 快捷切换饼干</label><input type="checkbox" id="sp_enableCookieConfirm" class="xdex-switch" role="switch"><label for="sp_enableCookieConfirm"> 二次确认饼干</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enablePaginationDuplication" class="xdex-switch" role="switch"><label for="sp_enablePaginationDuplication"> 添加页首页码</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_disableWatermark" class="xdex-switch" role="switch"><label for="sp_disableWatermark"> 关闭图片水印</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_updatePreviewCookie" class="xdex-switch" role="switch"><label for="sp_updatePreviewCookie"> 预览真实饼干</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_hideEmptyTitleEmail" class="xdex-switch" role="switch"><label for="sp_hideEmptyTitleEmail"> 隐藏无标题/无名氏/版规</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableExternalImagePreview" class="xdex-switch" role="switch"><label for="sp_enableExternalImagePreview"> 显示外部图床</label></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enableAutoCookieRefresh" class="xdex-switch" role="switch"><label for="sp_enableAutoCookieRefresh"> 自动刷新饼干</label><input type="checkbox" id="sp_enableAutoCookieRefreshToast" class="xdex-switch" role="switch"><label for="sp_enableAutoCookieRefreshToast"> toast提示</label></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enableSeamlessPaging" class="xdex-switch" role="switch"><label for="sp_enableSeamlessPaging"> 无缝翻页</label><input type="checkbox" id="sp_enableAutoSeamlessPaging" class="xdex-switch" role="switch" checked><label for="sp_enableAutoSeamlessPaging"> 自动翻页</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableHDImageAndLayoutFix" class="xdex-switch" role="switch"><label for="sp_enableHDImageAndLayoutFix"> 图片控件-布局调整</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableLinkBlank" class="xdex-switch" role="switch"><label for="sp_enableLinkBlank"> 新标签打开串</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableAutoUrlLinkify" class="xdex-switch" role="switch"><label for="sp_enableAutoUrlLinkify"> 自动识别链接</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableQuotePreview" class="xdex-switch" role="switch"><label for="sp_enableQuotePreview"> 优化引用弹窗</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_extendQuote" class="xdex-switch" role="switch"><label for="sp_extendQuote"> 拓展引用格式</label><input type="checkbox" id="sp_extendQuoteAvailabilityDetection" class="xdex-switch" role="switch"><label for="sp_extendQuoteAvailabilityDetection"> 可用性检测</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_toggleSidebar" class="xdex-switch" role="switch"><label for="sp_toggleSidebar"> 自动收起侧边栏</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableUpdateCheck" class="xdex-switch" role="switch"><label for="sp_enableUpdateCheck"> 检查更新</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableImageContextMenu" class="xdex-switch" role="switch"><label for="sp_enableImageContextMenu"> 图片菜单</label></div>
                <div style="${checkboxItemStyle}"><input type="checkbox" id="sp_enableThreadHistory" class="xdex-switch" role="switch"><label for="sp_enableThreadHistory"> 浏览历史</label></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enablePostHistory" class="xdex-switch" role="switch"><label for="sp_enablePostHistory"> 发言历史</label><input type="checkbox" id="sp_disableAutoQuote" class="xdex-switch" role="switch"><label for="sp_disableAutoQuote"> 关闭引用</label><select id="sp_postAfterAction" style="height:24px;"><option value="jump">发串后跳转</option><option value="refresh">发串后刷新</option></select></div>
                <!-- <div style="width:50%;"><label for="sp_占位"> </label></div> -->
                <!-- 以下是默认勾选项不可更改 -->
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_interceptReplyForm" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_interceptReplyForm"> 拦截回复中间页</label><input type="checkbox" id="sp_interceptReplyFormAutoCompress" class="xdex-switch" role="switch"><label for="sp_interceptReplyFormAutoCompress"> 自动压缩图片</label></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_interceptReplyFormUnvcode" class="xdex-switch" role="switch"><label for="sp_interceptReplyFormUnvcode"> unvcode</label><input type="checkbox" id="sp_interceptReplyFormU200B" class="xdex-switch" role="switch"><label for="sp_interceptReplyFormU200B"> 零宽空格优先</label></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_updateReplyNumbers" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_updateReplyNumbers"> 当页回复编号</label><input type="hidden" name="sp_updateReplyNumbers" value="1"></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_replaceRightSidebar" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_replaceRightSidebar"> 扩展坞增强</label><select id="sp_dockDisplayMode" style="height:24px;"><option value="hover">隐藏</option><option value="fixed">固定</option></select><input type="hidden" name="sp_replaceRightSidebar" value="1"></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_kaomojiEnhancer" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_kaomojiEnhancer"> 颜文字拓展</label><select id="sp_kaomojiSort" style="height:24px;"><option value="default">默认</option><option value="recent">最近</option><option value="freq">常用</option></select><input type="hidden" name="sp_kaomojiEnhancer" value="1"></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_highlightPO" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_highlightPO"> 标记Po主</label><input type="hidden" name="sp_highlightPO" value="1"></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_applyFilters" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_applyFilters"> 标记/屏蔽-饼干/关键词</label><select id="sp_blockDisplayMode" style="height:24px;"><option value="fold">折叠</option><option value="hide">隐藏</option></select><input type="hidden" name="sp_applyFilters" value="1"></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_threadCookieWhitelistModeEnabled" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_threadCookieWhitelistModeEnabled"> 只看饼干</label><select id="sp_threadCookieWhitelistDisplayMode" style="height:24px;"><option value="fold">折叠</option><option value="hide">隐藏</option><option value="column">分栏</option></select><select id="sp_poAnnotationSideDisplayMode" style="height:24px;"><option value="collapse">收起</option><option value="expand">展开</option></select></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enhancePostFormLayout" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_enhancePostFormLayout"> 发串UI调整</label><input type="hidden" name="sp_enhancePostFormLayout" value="1"></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enhanceIsland" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_enhanceIsland"> 增强X岛匿名版</label><select id="sp_enableDraftMode" style="height:24px;"><option value="on">关闭草稿</option><option value="off">开启草稿</option></select><select id="sp_timeDisplayMode" style="height:24px;"><option value="relative">相对时间</option><option value="exact">精确时间</option></select><input type="hidden" name="sp_enhanceIsland" value="1"></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enablePostExpand" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_enablePostExpand"> 展开板块页长串</label><select id="sp_postExpandAllMode" style="height:24px;"><option value="collapse">全部收起</option><option value="expand">全部展开</option></select><input type="hidden" name="sp_enablePostExpand" value="1"></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_searchServiceBy4sY" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_searchServiceBy4sY"> 野生搜索酱</label><input type="hidden" name="sp_searchServiceBy4sY" value="1"></div>
                <div style="${checkboxRowStyle}">
                  <input type="checkbox" id="sp_enableImageHideMode" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_enableImageHideMode"> 图片显示模式</label>
                  <select id="sp_applyImageHideMode" style="height:24px;">
                    <option value="default">默认</option>
                    <option value="blur">模糊</option>
                    <option value="noimage">无图</option>
                    <option value="tips">Tips</option>
                  </select>
                </div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enableFavoriteThreads" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_enableFavoriteThreads"> 常用串</label></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enableSubscriptionFeed" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_enableSubscriptionFeed"> 我的订阅</label><button type="button" id="sp_openDefaultSubscriptionFeed" class="xdex-edit-default-btn" style="margin-left:4px;" title="设置默认订阅号">${XDEX_ICON_GEAR}</button></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enableImageViewerMode" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_enableImageViewerMode"> 阅图模式</label><input type="checkbox" id="sp_imageViewerSeparators" class="xdex-switch" role="switch"><label for="sp_imageViewerSeparators"> 分隔线</label><button type="button" id="sp_openImageViewer" class="xdex-icon-btn" style="margin-left:4px;" title="打开阅图模式">${XDEX_ICON_IMAGE}</button></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_autoSelectReportReason" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_autoSelectReportReason"> 值班室优化</label></div>
                <div style="${checkboxRowStyle}"><input type="checkbox" id="sp_enableThreadExport" class="xdex-switch fixed-on" role="switch" checked disabled><label for="sp_enableThreadExport"> 导出串</label><button type="button" id="sp_openThreadExport" class="xdex-icon-btn" style="margin-left:4px;" title="打开导出窗口">${XDEX_EXPORT_ICON}</button></div>
            </div>
              <div style="margin-top:12px;">
                <h3 id="sp_replyQuicklyOnBoardPage" style="margin:6px 0;">板块页快速回复默认设置</h3>
                <div style="${quickReplyRowStyle}">
                  <!-- 左侧:板块页默认模式 -->
                  <div style="${quickReplyColumnStyle}">
                    <label for="sp_replyModeDefault" style="margin-bottom:4px;">板块页默认模式</label>
                    <select id="sp_replyModeDefault" style="width:100%;">
                      <option value="发串">发串</option>
                      <option value="回复">回复</option>
                    </select>
                  </div>
                  <!-- 右侧:回复默认模式 -->
                  <div style="${quickReplyColumnStyle}">
                    <label for="sp_replyExtraDefault" style="margin-bottom:4px;">回复默认模式</label>
                    <select id="sp_replyExtraDefault" style="width:100%;">
                      <option value="临时">临时</option>
                      <option value="连续">连续</option>
                    </select>
                  </div>
                </div>
                  </div>
              <div style="margin-top:12px;">
                <!-- 标记饼干(组) -->
                  <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_marked,#btn_group_marked"
                      style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>标记饼干</span>
                    <button id="btn_group_marked" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:auto;" title="添加分组">${XDEX_ICON_ADD_GROUP}</button>
                    <button id="btn_sp_marked" class="sp_save xdex-btn-hidden xdex-icon-btn" data-id="sp_marked" style="margin-left:4px;" title="保存">${XDEX_ICON_SAVE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                    <div id="marked-inputs-container"></div>
                  </div>
                </div>
                <!-- 屏蔽饼干(组,含备注) -->
                  <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_blocked,#btn_group_blocked"
                      style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>屏蔽饼干</span>
                    <button id="btn_group_blocked" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:auto;" title="添加分组">${XDEX_ICON_ADD_GROUP}</button>
                    <button id="btn_sp_blocked" class="sp_save xdex-btn-hidden xdex-icon-btn" data-id="sp_blocked" style="margin-left:4px;" title="保存">${XDEX_ICON_SAVE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                    <div id="blocked-inputs-container"></div>
                  </div>
                </div>
                <!-- 只看饼干(组,按串号) -->
                  <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_threadCookieWhitelist,#btn_group_threadCookieWhitelist"
                      style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>只看饼干</span>
                    <button id="btn_group_threadCookieWhitelist" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:auto;" title="添加分组">${XDEX_ICON_ADD_GROUP}</button>
                    <button id="btn_sp_threadCookieWhitelist" class="sp_save xdex-btn-hidden xdex-icon-btn" data-id="sp_threadCookieWhitelist" style="margin-left:4px;" title="保存">${XDEX_ICON_SAVE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                    <div id="thread-cookie-whitelist-inputs-container"></div>
                  </div>
                </div>
                <!-- 屏蔽关键词(组) -->
                <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_blockedKeywords,#btn_group_blockedKeywords"
                      style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>屏蔽关键词</span>
                    <button id="btn_group_blockedKeywords" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:auto;" title="添加分组">${XDEX_ICON_ADD_GROUP}</button>
                    <button id="btn_sp_blockedKeywords" class="sp_save xdex-btn-hidden xdex-icon-btn" data-id="sp_blockedKeywords" style="margin-left:4px;" title="保存">${XDEX_ICON_SAVE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                    <div id="blocked-keyword-inputs-container"></div>
                    <div style="font-size:12px;color:#888;text-align:center;">默认逗号分隔关键词;开启“正则”后整组按正则匹配(不再逗号切分)</div>
                  </div>
                </div>
                <!-- 常用串 -->
                <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_favoriteThreads,#btn_group_favoriteThreads"
                      style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>常用串</span>
                    <button id="btn_group_favoriteThreads" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:auto;" title="添加常用串">${XDEX_ICON_ADD_GROUP}</button>
                    <button id="btn_sp_favoriteThreads" class="sp_save xdex-btn-hidden xdex-icon-btn" data-id="sp_favoriteThreads" style="margin-left:4px;" title="保存">${XDEX_ICON_SAVE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                    <div id="favorite-thread-inputs-container"></div>
                    <!-- <div style="font-size:12px;color:#888;text-align:center;">备注可选;可填写 8 位串号或串链接,保存后统一为主串链接</div> -->
                  </div>
                </div>
                <!-- 我的订阅 -->
                <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_subscriptionFeeds,#btn_group_subscriptionFeeds"
                      style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>我的订阅</span>
                    <button id="btn_group_subscriptionFeeds" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:auto;" title="添加订阅号">${XDEX_ICON_ADD_GROUP}</button>
                    <button id="btn_sp_subscriptionFeeds" class="sp_save xdex-btn-hidden xdex-icon-btn" data-id="sp_subscriptionFeeds" style="margin-left:4px;" title="保存">${XDEX_ICON_SAVE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                    <div id="subscription-feed-inputs-container"></div>
                  </div>
                </div>
                <!-- 设置 导入/导出 -->
                <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_importExport"
                      style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>设置 导入/导出</span>
                    <button id="btn_sp_importExport" class="sp_save xdex-btn-hidden xdex-icon-btn" data-id="sp_importExport" style="margin-left:auto;" title="应用">${XDEX_ICON_SAVE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                    <div style="display:flex;gap:8px;margin-bottom:8px;">
                      <button id="sp_importClipboard" style="flex:1;padding:4px 8px;font-size:13px;cursor:pointer;">从剪贴板导入</button>
                      <button id="sp_exportClipboard" style="flex:1;padding:4px 8px;font-size:13px;cursor:pointer;">导出到剪贴板</button>
                    </div>
                    <div style="display:flex;gap:8px;margin-bottom:8px;">
                      <button id="sp_importFile" style="flex:1;padding:4px 8px;font-size:13px;cursor:pointer;">从文件导入</button>
                      <button id="sp_exportFile" style="flex:1;padding:4px 8px;font-size:13px;cursor:pointer;">导出为文件</button>
                    </div>
                    <div style="font-size:12px;color:#888;text-align:center;">导入将覆盖当前全部配置,建议先导出备份</div>
                  </div>
                </div>
                <!-- 使用数据 导入/导出 -->
                <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_fullExport_reset,#btn_sp_fullExport_export,#btn_sp_fullExport_import" style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>使用数据 导入/导出</span>
                    <button id="btn_sp_fullExport_reset" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:auto;" title="重置所选项目">${XDEX_ICON_RESET}</button>
                    <button id="btn_sp_fullExport_export" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:4px;" title="导出为文件">${XDEX_ICON_EXPORT_FILE}</button>
                    <button id="btn_sp_fullExport_import" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:4px;" title="从文件导入">${XDEX_ICON_IMPORT_FILE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                    <div style="font-size:12px;color:#666;margin-bottom:6px;">
                      用于备份或迁移脚本/扩展运行产生的数据,支持按类别导出与导入。默认全选。
                    </div>
                    <div style="display:flex;flex-wrap:wrap;gap:6px 16px;margin-bottom:6px;">
                      <div style="display:flex;align-items:center;gap:4px;">
                        <input type="checkbox" id="sp_fullExport_settings" class="xdex-switch" role="switch" checked>
                        <label for="sp_fullExport_settings">设置</label>
                      </div>
                      <div style="display:flex;align-items:center;gap:4px;">
                        <input type="checkbox" id="sp_fullExport_threadHistory" class="xdex-switch" role="switch" checked>
                        <label for="sp_fullExport_threadHistory">浏览历史</label>
                      </div>
                      <div style="display:flex;align-items:center;gap:4px;">
                        <input type="checkbox" id="sp_fullExport_postHistory" class="xdex-switch" role="switch" checked>
                        <label for="sp_fullExport_postHistory">发言历史</label>
                      </div>
                      <div style="display:flex;align-items:center;gap:4px;">
                        <input type="checkbox" id="sp_fullExport_drafts" class="xdex-switch" role="switch" checked>
                        <label for="sp_fullExport_drafts">草稿</label>
                      </div>
                      <div style="display:flex;align-items:center;gap:4px;">
                        <input type="checkbox" id="sp_fullExport_kaomojiStats" class="xdex-switch" role="switch" checked>
                        <label for="sp_fullExport_kaomojiStats">颜文字统计</label>
                      </div>
                      <div style="display:flex;align-items:center;gap:4px;">
                        <input type="checkbox" id="sp_fullExport_cookiePrefs" class="xdex-switch" role="switch" checked>
                        <label for="sp_fullExport_cookiePrefs">串内饼干偏好</label>
                      </div>
                      <div style="display:flex;align-items:center;gap:4px;">
                        <input type="checkbox" id="sp_fullExport_webdav" class="xdex-switch" role="switch" checked>
                        <label for="sp_fullExport_webdav">WebDAV 配置</label>
                      </div>
                    </div>
                    <div id="sp_fullExport_import_preview" style="display:none;margin-top:8px;padding:6px 8px;border:1px dashed #aaa;border-radius:6px;background:#FFFFEE;"></div>
                  </div>
                </div>
                <!-- WebDAV 备份/同步 -->
                <div class="sp_fold" style="border:1px solid #eee;margin:6px 0;background:#F0E0D6;">
                  <div class="sp_fold_head" data-btn="#btn_sp_webdavSave,#btn_webdavSync,.xdex-webdav-head-actions"
                      style="display:flex;align-items:center;padding:6px 8px;background:#F0E0D6;cursor:pointer;">
                    <span>WebDAV 备份/同步</span>
                    <span id="sp_webdavLastSyncLabel" style="flex:1;min-width:0;margin:0 8px;text-align:center;color:#666;font-size:12px;">${webdavPanelField('lastSync')}</span>
                    <div class="xdex-webdav-head-actions xdex-inv" style="display:flex;align-items:center;gap:4px;margin-left:0;" title="自动同步策略:&#10;· 所有页面共享一个计时器,约1小时触发一次&#10;· 到点没有页面同步时,打开新页面会立即补一次&#10;· 点击手动同步后计时器会重置&#10;· 开启开关后立即同步一次(60秒内不重复,内容无变化也不重复)&#10;&#10;同步策略:&#10;· 远端较新则下载合并&#10;· 设置冲突时自动保留更合理的版本(本地为默认则采用远端,已个性化则保留本地)&#10;· WebDAV 配置不随同步覆盖">
                      <label for="sp_webdavAutoSync" style="font-size:12px;">自动同步</label>
                      <input type="checkbox" id="sp_webdavAutoSync" class="xdex-switch" role="switch" ${webdavPanelField('autoSync')}>
                    </div>
                    <button id="btn_webdavSync" type="button" class="xdex-btn-hidden xdex-icon-btn" style="margin-left:4px;" title="手动同步">${XDEX_ICON_SYNC}</button>
                    <button id="btn_sp_webdavSave" class="sp_save xdex-btn-hidden xdex-icon-btn" data-id="sp_webdavSave" style="margin-left:4px;" title="保存">${XDEX_ICON_SAVE}</button>
                  </div>
                  <div class="sp_fold_body" style="display:none;padding:8px 10px;background:#F0E0D6;">
                        <div style="display:flex;gap:4px;align-items:center;">
                          <input id="sp_webdavUrl" type="text" value="${webdavPanelField('url')}" placeholder="WebDAV 链接(目录,如 https://dav.jianguoyun.com/dav/xdex)" style="flex:1;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;">
                          <button id="btn_webdavCheck" type="button" class="xdex-icon-btn" title="检查连接">${XDEX_ICON_PLUG}</button>
                        </div>
                        <div style="display:flex;gap:4px;align-items:center;">
                          <input id="sp_webdavUsername" type="text" value="${webdavPanelField('username')}" placeholder="账户" style="flex:1;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;">
                          <span class="xdex-icon-btn" style="opacity:0;pointer-events:none;" aria-hidden="true"></span>
                        </div>
                        <div style="display:flex;gap:4px;align-items:center;">
                          <input id="sp_webdavPassword" type="password" value="${webdavPanelField('password')}" placeholder="密码" style="flex:1;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;">
                          <button id="btn_webdavTogglePassword" type="button" class="xdex-icon-btn" title="显示密码">${XDEX_ICON_EYE}</button>
                        </div>
                        <div id="sp_webdavStatus" style="font-size:12px;color:#666;white-space:pre-wrap;margin-top:8px;"></div>
                  </div>
                </div>
                </div>
              </div>
              </div>
              <div id="sp_module_history" class="sp_panel_module" data-sp-module-view="history">
                <div class="sp_panel_content">
                  <div id="sp_history_content">
                    <div id="sp_history_title" style="margin:0 0 10px; position:relative; text-align:center;">
                      <span style="font-size:20px; font-weight:bold;">浏览历史</span>
                    </div>
                    <div class="xdex-history-toolbar" id="sp_history_toolbar">
                      <input id="sp_history_search" type="search" autocomplete="off" placeholder="搜索标题、名称、正文、串号等关键词;高级检索见后方 ?">
                      <span id="sp_history_count" class="xdex-history-count">0 条</span>
                      <select id="sp_history_sort" aria-label="浏览历史排序">
                        <option value="last-desc">最近访问优先</option>
                        <option value="last-asc">最早访问优先</option>
                        <option value="visits-desc">访问次数最多</option>
                        <option value="visits-asc">访问次数最少</option>
                        <option value="page-desc">最高页码优先</option>
                      </select>
                      <button id="sp_history_clear" type="button" style="padding:6px 10px;">清空</button>
                      <button id="sp_history_recycle" type="button" class="xdex-recycle-btn" title="回收站" aria-label="回收站">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
                        <span id="sp_history_recycle_badge" class="xdex-recycle-badge" hidden>0</span>
                      </button>
                    </div>
                  <div id="sp_history_recycle_bar" class="xdex-recycle-bar" hidden>
                    <button id="sp_history_recycle_back" type="button" class="xdex-recycle-back" title="返回浏览历史">← 返回</button>
                    <span class="xdex-recycle-bar-title">回收站 <span id="sp_history_recycle_barcount" class="xdex-history-count">0 条</span></span>
                    <select id="sp_history_recycle_sort" class="xdex-recycle-sort" aria-label="回收站排序">
                      <option value="expiring">即将删除优先</option>
                      <option value="recent">最近删除优先</option><option value="due24h">24小时内到期</option>
                    </select>
                    <button id="sp_history_recycle_empty" type="button" class="xdex-recycle-empty-btn" disabled>清空回收站</button>
                  </div>
                    <div id="sp_history_results"></div>
                  </div>
                </div>
              </div>
              <div id="sp_module_posts" class="sp_panel_module" data-sp-module-view="posts">
                <div class="sp_panel_content">
                  <div id="sp_posts_content">
                    <div id="sp_posts_title" style="margin:0 0 10px; position:relative; text-align:center;">
                      <span style="font-size:20px; font-weight:bold;">我的发言</span>
                    </div>
                    <div class="xdex-history-toolbar" id="sp_posts_toolbar">
                      <input id="sp_posts_search" type="search" autocomplete="off" placeholder="搜索标题、名称、正文、串号等关键词;高级检索见后方 ?">
                      <span id="sp_posts_count" class="xdex-history-count">0 条</span>
                      <button id="sp_posts_clear" type="button" style="padding:6px 10px;">清空</button>
                      <button id="sp_posts_recycle" type="button" class="xdex-recycle-btn" title="回收站" aria-label="回收站">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
                        <span id="sp_posts_recycle_badge" class="xdex-recycle-badge" hidden>0</span>
                      </button>
                    </div>
                  <div id="sp_posts_recycle_bar" class="xdex-recycle-bar" hidden>
                    <button id="sp_posts_recycle_back" type="button" class="xdex-recycle-back" title="返回我的发言">← 返回</button>
                    <span class="xdex-recycle-bar-title">回收站 <span id="sp_posts_recycle_barcount" class="xdex-history-count">0 条</span></span>
                    <select id="sp_posts_recycle_sort" class="xdex-recycle-sort" aria-label="回收站排序">
                      <option value="expiring">即将删除优先</option>
                      <option value="recent">最近删除优先</option><option value="due24h">24小时内到期</option>
                    </select>
                    <button id="sp_posts_recycle_empty" type="button" class="xdex-recycle-empty-btn" disabled>清空回收站</button>
                  </div>
                    <div id="sp_posts_type_buttons" class="xdex-post-history-type-buttons">
                      <button type="button" data-post-history-type="thread">我的主题</button>
                      <button type="button" data-post-history-type="reply" class="active">我的回复</button>
                    </div>
                    <div class="xdex-posts-add-row" style="display:flex;gap:6px;margin-bottom:8px;">
                      <input id="sp_posts_manual_add_input" type="search" placeholder="No.67024789、67024789、https://nmbxd1.com/t/67024789、67024789?r=68811442&page=23" style="flex:1;padding:4px 8px;font-size:12px;border:1px solid var(--xdex-sp-border, #ccc);border-radius:6px;background:var(--xdex-sp-panel-bg, #fff);">
                      <button id="sp_posts_manual_add_btn" type="button" style="padding:4px 10px;font-size:13px;">手动添加</button>
                    </div>
                    <div id="sp_posts_results"></div>
                  </div>
                </div>
              </div>
              <div id="sp_module_feeds" class="sp_panel_module" data-sp-module-view="feeds">
                <div class="sp_panel_content">
                  <div id="sp_feeds_content">
                    <div id="sp_feeds_title" style="margin:0 0 10px; position:relative; text-align:center;">
                      <span style="font-size:20px; font-weight:bold;">我的订阅</span>
                    </div>
                    <div class="xdex-history-toolbar" style="flex-wrap:wrap;gap:6px;">
                      <div class="xdex-feed-selector-wrap" style="flex:1 1 auto;min-width:0;position:relative;">
                      <select id="sp_feeds_selector" aria-hidden="true" tabindex="-1" style="position:absolute;opacity:0;pointer-events:none;width:0;height:0;"></select>
                      <div id="sp_feeds_selector_display" class="xdex-feed-selector-display" role="combobox" aria-haspopup="listbox" aria-expanded="false">
                        <span class="xdex-feed-display-desc"></span>
                        <span class="xdex-feed-display-uuid"></span>
                        <svg class="xdex-feed-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M8 2.5L4 6L8 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
                      </div>
                      <div id="sp_feeds_selector_dropdown" class="xdex-feed-selector-dropdown" role="listbox" style="display:none;"></div>
                    </div>
                      <input id="sp_feeds_page_input" type="number" min="1" placeholder="页码" size="4" style="flex:0 0 auto;width:4em;padding:4px 6px;">
                      <button id="sp_feeds_page_jump" type="button" style="padding:4px 8px;">跳转</button>
                    </div>
                    <div style="display:flex;gap:6px;margin-bottom:8px;">
                      <input id="sp_feeds_bulk_add_input" type="search" placeholder="输入串号,多个用逗号分隔,如 67024789,66994128" autocomplete="off" style="flex:1;padding:4px 8px;font-size:12px;border:1px solid var(--xdex-sp-border, #ccc);border-radius:6px;background:var(--xdex-sp-panel-bg, #fff);">
                      <button id="sp_feeds_bulk_add_btn" type="button" style="padding:4px 10px;font-size:13px;">批量添加</button>
                      <button id="sp_feeds_export_clipboard" type="button" style="padding:4px 10px;font-size:13px;">导出串号</button>
                    </div>
                    <div id="sp_feeds_results"></div>
                  </div>
                </div>
              </div>
            </div>
            <div id="sp_panel_footer" style="padding:10px 18px;display:flex;align-items:center;justify-content:space-between;position:relative;border-top:1px solid #eee;background:#FFFFEE;">
              <div class="sp_panel_links" style="display:flex;align-items:center;gap:8px;">
                <a data-update-channel="thread" data-thread-id="67024789" href="https://www.nmbxd1.com/t/67024789" target="_blank" rel="noopener" title="更新串内">串内</a>
                <a data-update-channel="greasyfork" href="https://greasyfork.org/zh-CN/scripts/531005-x%E5%B2%9B-ex" target="_blank" rel="noopener" title="GreasyFork渠道,需代理">GreasyFork</a>
                <a data-update-channel="github" href="https://github.com/SayaGoodBye/nmbxd-EX" target="_blank" rel="noopener" title="Github">Github</a>
                <a data-update-channel="scriptcat" href="https://scriptcat.org/zh-CN/script-show-page/6289" target="_blank" rel="noopener" title="脚本猫渠道,可直连">ScriptCat</a>
                <a data-update-channel="baidupan" href="https://pan.baidu.com/s/1-ELWglsTXG8jK5S6WwqtsQ?pwd=k8zf" target="_blank" rel="noopener" title="百度网盘备份">百度网盘</a>
                <a data-update-channel="release" href="https://fastly.jsdelivr.net/gh/SayaGoodBye/nmbxd-EX@latest/nmbxd-EX.user.js" target="_blank" rel="noopener" title="最新发布版用户脚本镜像">Release</a>
                <a data-update-channel="dev" href="https://fastly.jsdelivr.net/gh/SayaGoodBye/nmbxd-EX@dev/nmbxd-EX.user.js" target="_blank" rel="noopener" title="最新开发版用户脚本镜像">Dev</a>
                <!-- 镜像服务:https://docs.suanlix.cn/github.html -->
                <a data-update-channel="extension" href="https://github.ikgy.top/https://github.com/SayaGoodBye/nmbxd-EX/releases/latest/download/nmbxd-EX-Extension.zip" target="_blank" rel="noopener" title="最新发布版Chromium内核浏览器拓展">拓展</a>
              </div>
              <div id="sp_feeds_pager" style="display:none;position:absolute;left:50%;transform:translateX(-50%);align-items:center;gap:8px;">
                <button id="sp_feeds_prev" type="button" style="padding:4px 10px;">上一页</button>
                <span id="sp_feeds_page_label" style="font-size:18px;color:#666;">第1页</span>
                <button id="sp_feeds_next" type="button" style="padding:4px 10px;">下一页</button>
              </div>
              <div class="sp_panel_actions" style="display:flex;align-items:center;gap:10px;">
                <button id="sp_apply" style="padding:6px 10px;">保存并刷新</button>
                <button id="sp_close" style="padding:6px 10px;">关闭</button>
              </div>
            </div>
          </div>
        </div>`;
      $('#sp_cover').remove();
      $('body').append(html);
      syncFavoriteThreadsLinks();
      installSettingPanelEasterEgg(document);
      function setSettingsPanelModule(moduleName) {
        // 离开对应模块时退出其回收站视图,回到正常列表态
        if (moduleName !== 'history') setThreadHistoryRecycleMode(false);
        if (moduleName !== 'posts') setPostHistoryRecycleMode(false);
        const $nextView = $(`#sp_panel_views [data-sp-module-view="${moduleName}"]`);
        const nextModule = $nextView.length ? moduleName : 'settings';
        $('#sp_panel_tab_slot .sp_panel_tab').removeClass('active')
          .filter(`[data-sp-module="${nextModule}"]`).addClass('active');
        $('#sp_panel_views .sp_panel_module').removeClass('active').css('display', 'none');
        const $activeModule = $('#sp_panel_views .sp_panel_module')
          .filter(`[data-sp-module-view="${nextModule}"]`)
          .addClass('active')
          .css({ display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: (nextModule === 'history' || nextModule === 'posts' || nextModule === 'feeds') ? '300px' : '0' });
        if (nextModule === 'history' || nextModule === 'posts' || nextModule === 'feeds') {
          $activeModule.find('.sp_panel_content').css({ display: 'block', flex: '1 1 auto', minHeight: '300px', overflowY: 'auto', boxSizing: 'border-box' });
          $activeModule.find('#sp_history_content,#sp_posts_content,#sp_feeds_content').css({ display: 'block' });
          $activeModule.find('#sp_history_results,#sp_posts_results,#sp_feeds_results').css({ display: 'block', minHeight: '40px' });
        }
        $('#sp_panel_footer .sp_panel_links').show();
        $('#sp_panel_footer .sp_panel_links a').toggle(nextModule === 'settings');
        $('#sp_apply').toggle(nextModule === 'settings');
        $('#sp_feeds_pager').toggle(nextModule === 'feeds');
        updateThreadHistoryDebugState({
          lastPanelModule: nextModule,
          lastPanelState: {
            requested: moduleName,
            resolved: nextModule,
            footerLinksVisible: $('#sp_panel_footer .sp_panel_links').is(':visible'),
            applyVisible: $('#sp_apply').is(':visible'),
            historyResults: !!document.getElementById('sp_history_results'),
            historyResultsHeight: document.getElementById('sp_history_results')?.offsetHeight || 0,
            historyModuleDisplay: getComputedStyle(document.getElementById('sp_module_history') || document.body).display,
            historyContentDisplay: getComputedStyle(document.getElementById('sp_history_content') || document.body).display,
            historyPanelContentHeight: document.querySelector('#sp_module_history .sp_panel_content')?.offsetHeight || 0,
            at: new Date().toISOString()
          }
        });
        logThreadHistory('panel module switched', threadHistoryDebugState.lastPanelState);
        logThreadHistoryFlat('panel module switched flat', threadHistoryDebugState.lastPanelState);
        // 进入浏览/发言历史 tab 时自动聚焦搜索框:
        // 用户可直接键入搜索;焦点从搜索框出发,Tab 仍按序进入列表(键盘移动列表不受干扰)。
        // focus({preventScroll:true}) 避免面板内容区被滚动到顶部。
        if (nextModule === 'history' || nextModule === 'posts') {
          const searchInput = document.getElementById(nextModule === 'history' ? 'sp_history_search' : 'sp_posts_search');
          if (searchInput) {
            try { searchInput.focus({ preventScroll: true }); } catch (e) { try { searchInput.focus(); } catch (_e) {} }
          }
        }
      }
      $('#sp_panel_tab_slot').off('click', '[data-sp-module]').on('click', '[data-sp-module]', (e) => {
        e.preventDefault();
        logThreadHistory('panel tab clicked', { module: $(e.currentTarget).data('spModule') });
        const clickedModule = $(e.currentTarget).data('spModule');
        // 功能关闭时进入对应 tab:error toast 提示不再接受新数据写入(数据仍可查看)
        if (clickedModule === 'history' && !isThreadHistoryEnabledNow()) {
          toast('浏览历史已关闭:不会记录新的浏览数据,已有数据仍可查看', 0, { type: 'error', key: 'thread-history-disabled' });
        } else if (clickedModule === 'posts' && !isPostHistoryEnabledNow()) {
          toast('发言历史已关闭:不会自动记录新的发言数据,已有数据仍可查看', 0, { type: 'error', key: 'post-history-disabled' });
        }
        setSettingsPanelModule(clickedModule);
        if ($(e.currentTarget).data('spModule') === 'history') renderThreadHistoryModuleSoon();
        if ($(e.currentTarget).data('spModule') === 'posts') renderPostHistoryModuleSoon();
        if ($(e.currentTarget).data('spModule') === 'feeds') renderSubscriptionFeedModule();
      });
      $('#sp_panel_tab_slot').off('mouseenter mouseleave', '.sp_panel_tab')
        .on('mouseenter', '.sp_panel_tab', (e) => { $(e.currentTarget).addClass('is-hover'); })
        .on('mouseleave', '.sp_panel_tab', (e) => { $(e.currentTarget).removeClass('is-hover'); });
      setSettingsPanelModule('settings');
      bindThreadHistoryModuleEvents();
      bindThreadHistoryLiveSync();
      bindPostHistoryModuleEvents();
      bindPostHistoryLiveSync();
      bindSubscriptionFeedModuleEvents();
      // 浏览/发言历史改为首次进入对应 tab 时再渲染,避免 init 阶段双模块预构建卡顿
      // 折叠头:统一控制
      $('.sp_fold_head').off('click').on('click', function(){
        // WebDAV 头部的自动同步开关与保存按钮等控件不触发折叠
        if ($(this).closest('.xdex-webdav-head-actions').length || this.id === 'btn_sp_webdavSave') return;
        const $head = $(this);
        $head.next('.sp_fold_body').slideToggle(150);
        const btns = ($head.data('btn') || '').split(',');
        btns.forEach(sel => {
          const $el = $(sel);
          // 图标按钮用 xdex-btn-hidden(display:none),文本按钮沿用 xdex-inv(透明热区)
          $el.toggleClass($el.hasClass('xdex-icon-btn') ? 'xdex-btn-hidden' : 'xdex-inv');
        });
      });
      // 控件交互独立于折叠: 阻止冒泡到 .sp_fold_head
      $('#sp_webdavAutoSync, label[for="sp_webdavAutoSync"], #btn_sp_webdavSave').off('click.xdex-webdav-fold-guard').on('click.xdex-webdav-fold-guard', function (e) {
        e.stopPropagation();
      });
      // 同步已有配置 & 默认折叠
      this.syncInputs();
      const reloadRequiredSettingKeys = [
        'enableCookieSwitch',
        'disableWatermark',
        'enablePaginationDuplication',
        'updatePreviewCookie',
        'hideEmptyTitleEmail',
        'enableExternalImagePreview',
        'enableUpdateCheck',
        'enableAutoCookieRefresh',
        'enableAutoCookieRefreshToast',
        'enableAutoSeamlessPaging',
        'enableHDImageAndLayoutFix',
        'enableImageContextMenu',
        'enableLinkBlank',
        'enableAutoUrlLinkify',
        'enableQuotePreview',
        'toggleSidebar',
        'enableCustomDarkTheme'
      ];
      // 即时保存项:从"刷新提示"清单移出(change 即保存),但「保存并刷新」按钮仍须收集它们,
      // 保证无论用户是否触发过 change,点击按钮后所有面板修改都在刷新后生效
      const immediateSaveSettingKeys = [
        'enableCookieConfirm',
        'interceptReplyFormUnvcode',
        'interceptReplyFormU200B',
        'interceptReplyFormAutoCompress',
        'disableAutoQuote',
        'extendQuote',
        'extendQuoteAvailabilityDetection',
        'enableSeamlessPaging'
      ];
      const collectReloadRequiredSettingsFromPanel = () => {
        reloadRequiredSettingKeys.forEach(k => { this.state[k] = $('#sp_' + k).is(':checked'); });
        immediateSaveSettingKeys.forEach(k => { this.state[k] = $('#sp_' + k).is(':checked'); });
        // 深色模式:布尔开关 → light/dark 语义值
        this.state.enableCustomDarkTheme = $('#sp_enableCustomDarkTheme').is(':checked') ? 'dark' : 'light';
        // 固定启用:不受面板勾选状态影响
        this.state.enableImageHideMode = true;
      };
      // 设置写入失败会造成内存与持久化状态失步且完全无感知;留一次告警痕迹
      // 标志挂 window:本方法可被多次执行,局部变量会随每次重开面板而复位
      const warnSettingSaveFailed = (where, e) => {
        if (window.__xdexSettingSaveWarned) return;
        window.__xdexSettingSaveWarned = true;
        console.warn('[X岛-EX] 设置写入失败,刷新后该设置可能丢失(' + where + ')', e);
      };
      const saveReloadRequiredSettingsImmediately = () => {
        collectReloadRequiredSettingsFromPanel();
        try {
          GM_setValue(this.key, this.state);
          // 深色模式等即时生效项:保存后立即同步主题(无需刷新页面)
          if (typeof window.__xdexSyncDarkReaderTheme === 'function') window.__xdexSyncDarkReaderTheme();
          toast('设置已保存,刷新后生效', 900, { queue: false, key: 'settings-saved' });
        } catch (e) { warnSettingSaveFailed('需刷新项即时保存', e); }
      };
      const reloadRequiredSettingSelector = reloadRequiredSettingKeys.map(k => '#sp_' + k).join(',');
      $(reloadRequiredSettingSelector)
        .off('change.xdexReloadSettingSave')
        .on('change.xdexReloadSettingSave', saveReloadRequiredSettingsImmediately);
      // 历史开关:即时生效(无需刷新)——更新 state + 持久化 + toast + 重渲染侧边栏入口
      const applyHistoryToggleImmediately = (key, label) => {
        const checked = $('#sp_' + key).is(':checked');
        this.state[key] = checked;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed(label, e); }
        toast(label + (checked ? '已开启' : '已关闭'), 1400, { queue: false, key: 'history-toggle' });
        try { renderFavoriteThreadsMenu(); } catch (e) {}
      };
      $('#sp_enableThreadHistory').off('change.xdexHistoryToggle').on('change.xdexHistoryToggle', () => {
        applyHistoryToggleImmediately('enableThreadHistory', '浏览历史');
      });
      $('#sp_enablePostHistory').off('change.xdexHistoryToggle').on('change.xdexHistoryToggle', () => {
        applyHistoryToggleImmediately('enablePostHistory', '发言历史');
      });
      // ★ 发送链开关:即时保存(消费点每次从 GM 重读:doSubmit / finishSubmitImageProcessing)
      // ——无需刷新,下次发送即生效;跨页同步由 GM 存储监听自然完成。
      const saveSendChainToggleImmediately = (key, label) => {
        const checked = $('#sp_' + key).is(':checked');
        this.state[key] = checked;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed(label, e); }
        toast(label + (checked ? '已开启' : '已关闭') + '(下次发送生效)', 1400, { queue: false, key: 'send-chain-toggle' });
      };
      const sendChainToggleLabels = {
        interceptReplyFormUnvcode: 'unvcode',
        interceptReplyFormU200B: '零宽空格优先',
        interceptReplyFormAutoCompress: '自动压缩图片'
      };
      Object.keys(sendChainToggleLabels).forEach((key) => {
        $('#sp_' + key).off('change.xdexSendChainToggle').on('change.xdexSendChainToggle', () => {
          saveSendChainToggleImmediately(key, sendChainToggleLabels[key]);
        });
      });
      // ★ 发送前确认(串内饼干使用情况):即时保存 + 运行时注入/清理检查区
      $('#sp_enableCookieConfirm').off('change.xdexCookieConfirmToggle').on('change.xdexCookieConfirmToggle', () => {
        const checked = $('#sp_enableCookieConfirm').is(':checked');
        this.state.enableCookieConfirm = checked;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('发送前确认', e); }
        try {
          if (checked && this.state.enableCookieSwitch) {
            const tid = typeof PageType !== 'undefined' && PageType.getThreadId ? PageType.getThreadId(false) : '';
            const shortTid = tid ? String(tid).slice(0, 8) : '';
            if (shortTid) {
              if (typeof initThreadCookiePref === 'function') initThreadCookiePref(shortTid);
              if (typeof injectCookieCheckSwitch === 'function') injectCookieCheckSwitch(shortTid);
            }
          } else {
            if (typeof clearCookieCheckSwitch === 'function') clearCookieCheckSwitch();
          }
        } catch (e) {}
        toast('发送前确认' + (checked ? '已开启' : '已关闭') + '(下次发送生效)', 1400, { queue: false, key: 'cookie-confirm-toggle' });
      });
      // ★ 快速回复默认模式:即时保存(消费点读 SettingPanel.state:打开表单/切换模式时生效)
      const saveReplyDefaultImmediately = (key, label, isExtra) => {
        const val = $('#sp_' + key).val();
        this.state[key] = val;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed(label, e); }
        toast(label + '已保存:' + val, 1400, { queue: false, key: 'reply-default-toggle' });
      };
      $('#sp_replyModeDefault').off('change.xdexReplyDefault').on('change.xdexReplyDefault', () => {
        saveReplyDefaultImmediately('replyModeDefault', '板块页默认模式', false);
      });
      $('#sp_replyExtraDefault').off('change.xdexReplyDefault').on('change.xdexReplyDefault', () => {
        saveReplyDefaultImmediately('replyExtraDefault', '回复默认模式', true);
      });
      // ★ A 类即时项:消费点运行时读 GM/state,改完即生效
      // 关闭引用(载入编辑判定读 getFilterConfig(),每次读 GM)——完全即时
      $('#sp_disableAutoQuote').off('change.xdexImmediateToggle').on('change.xdexImmediateToggle', () => {
        const checked = $('#sp_disableAutoQuote').is(':checked');
        this.state.disableAutoQuote = checked;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('关闭引用', e); }
        toast('关闭引用' + (checked ? '已开启' : '已关闭') + '(下次插入引用号时生效)', 1400, { queue: false, key: 'immediate-toggle' });
      });
      // 引用号拓展 + 可用性检测:getQuoteAvailabilityState() 每次读最新 state → 新内容即时生效;
      // 开启时对当前页面重跑一次(幂等);关闭时当前页已有拓展保留至刷新(无文本备份,不可逆)
      const applyQuoteTogglesImmediately = (key, label) => {
        const checked = $('#sp_' + key).is(':checked');
        this.state[key] = checked;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed(label, e); }
        try {
          if (checked && typeof extendQuote === 'function') extendQuote(document);
          else if (typeof refreshQuoteAvailability === 'function') refreshQuoteAvailability(document);
        } catch (e) {}
        toast(label + (checked ? '已开启(已重新渲染当前页面引用号)' : '已关闭(当前页已有拓展将保留至刷新)'), 1400, { queue: false, key: 'immediate-toggle' });
      };
      $('#sp_extendQuote').off('change.xdexImmediateToggle').on('change.xdexImmediateToggle', () => {
        applyQuoteTogglesImmediately('extendQuote', '引用号拓展');
      });
      $('#sp_extendQuoteAvailabilityDetection').off('change.xdexImmediateToggle').on('change.xdexImmediateToggle', () => {
        applyQuoteTogglesImmediately('extendQuoteAvailabilityDetection', '引用号可用性检测');
      });
      // 无缝翻页:右栏「下一页」按钮路径(isSeamlessPagingEnabled 实时读 state)即时生效;
      // 触底观察器启动期装载,关闭本次不卸载(保留既有行为,toast 说明)
      $('#sp_enableSeamlessPaging').off('change.xdexImmediateToggle').on('change.xdexImmediateToggle', () => {
        const checked = $('#sp_enableSeamlessPaging').is(':checked');
        this.state.enableSeamlessPaging = checked;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('无缝翻页', e); }
        toast('无缝翻页' + (checked ? '已开启(下一页按钮即时生效)' : '已关闭(触底观察器保留至刷新)'), 1400, { queue: false, key: 'immediate-toggle' });
      });
      // 深色开关:切换时先加页面过渡类保证平滑,样式由 syncInputs/save 回调接管
      $('#sp_enableCustomDarkTheme')
        .off('change.xdexDarkToggle')
        .on('change.xdexDarkToggle', function () {
          try {
            document.documentElement.classList.add('xdex-theme-anim');
            syncDarkToggleVisual();
            setTimeout(() => { try { document.documentElement.classList.remove('xdex-theme-anim'); } catch (e) {} }, 500);
          } catch (e) {}
        });
      // 图片隐藏模式:即时切换并即时应用(无需点“应用更改”)
      const applyImageHideModeImmediately = () => {
        const mode = $('#sp_applyImageHideMode').val() || 'default';
        // 固定启用,仅切换具体模式
        this.state.enableImageHideMode = true;
        this.state.applyImageHideMode = mode;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('图片隐藏模式', e); }
        if (typeof applyImageHideMode === 'function') {
          applyImageHideMode(mode, document);
        }
      };
      // 扩展坞显示模式:即时切换并即时应用(固定/隐藏)
      const applyDockDisplayModeImmediately = () => {
        const mode = $('#sp_dockDisplayMode').val() || 'hover';
        this.state.dockDisplayMode = mode;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('扩展坞显示模式', e); }
        if (typeof applyDockDisplayMode === 'function') {
          applyDockDisplayMode(mode);
        }
      };
      $('#sp_dockDisplayMode').off('change').on('change', applyDockDisplayModeImmediately);
      $('#sp_enableImageHideMode').off('change').on('change', applyImageHideModeImmediately);
      $('#sp_applyImageHideMode').off('change').on('change', applyImageHideModeImmediately);
      // 阅图模式分隔线:即时切换并即时应用(无需点“应用更改”)
      const applyImageViewerSeparatorsImmediately = () => {
        const on = $('#sp_imageViewerSeparators').is(':checked');
        this.state.imageViewerSeparators = on;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('阅图分隔线', e); }
        if (typeof applyImageViewerSeparatorSetting === 'function') {
          applyImageViewerSeparatorSetting(on);
        }
        toast(on ? '已开启分隔线' : '已关闭分隔线', 900, { queue: false, key: 'image-viewer-separators' });
      };
      $('#sp_imageViewerSeparators').off('change').on('change', applyImageViewerSeparatorsImmediately);
      // 设置面板内打开阅图:串内页可用;无图模式下右上角按钮隐藏时的备选入口
      $('#sp_openImageViewer').off('click').on('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        if (typeof openImageViewerFromSettingsPanel === 'function') {
          openImageViewerFromSettingsPanel();
        } else if (typeof toast === 'function') {
          toast('当前页面不支持阅图模式');
        }
      });
      // 设置面板内打开导出窗口:串页可用(与「导」按钮同一入口)
      $('#sp_openThreadExport').off('click').on('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        try {
          if (typeof xdexExportRuntimeAvailable === 'function' && !xdexExportRuntimeAvailable()) {
            toast('导出需安装扩展版 X岛-EX,或脚本猫后台导出脚本');
            return;
          }
          if (typeof xdexExportOpenDialog === 'function') {
            xdexExportOpenDialog('');
          } else {
            toast('导出功能尚未就绪,请稍后重试');
          }
        } catch (err) {
          console.warn('[X岛-EX] 打开导出窗口失败', err);
          toast('打开导出窗口失败');
        }
      });
      // 屏蔽显示模式:即时切换并即时生效(折叠/隐藏)
      const applyBlockDisplayModeImmediately = () => {
        const mode = $('#sp_blockDisplayMode').val() || 'fold';
        this.state.blockDisplayMode = mode;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('屏蔽显示模式', e); }
        refreshFilterDisplay(this.state);
      };
      $('#sp_blockDisplayMode').off('change').on('change', applyBlockDisplayModeImmediately);
      const applyThreadCookieWhitelistDisplayModeImmediately = () => {
        this.state.threadCookieWhitelistDisplayMode = $('#sp_threadCookieWhitelistDisplayMode').val() || 'fold';
        this.state.poAnnotationSideDisplayMode = $('#sp_poAnnotationSideDisplayMode').val() || 'collapse';
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('串内饼干白名单显示模式', e); }
        refreshFilterDisplay(this.state);
      };
      $('#sp_threadCookieWhitelistDisplayMode').off('change').on('change', applyThreadCookieWhitelistDisplayModeImmediately);
      $('#sp_poAnnotationSideDisplayMode').off('change').on('change', applyThreadCookieWhitelistDisplayModeImmediately);
      // 颜文字排序:即时切换并即时生效(无需点“应用更改”)
      const applyKaomojiSortImmediately = () => {
        const mode = $('#sp_kaomojiSort').val() || 'default';
        this.state.kaomojiSort = mode;
        try { GM_setValue(this.key, this.state); } catch (e) { warnSettingSaveFailed('颜文字排序', e); }
        // 与颜文字按钮右侧的快捷下拉实时同步
        $('.sp_kaomojiSort_copy').val(mode);
        document.querySelectorAll('#h-emot-select').forEach(sel => {
          try {
            sel.dispatchEvent(new Event('kaomoji:sort-changed'));
          } catch (e) {}
        });
        // 广播排序模式变化,供其它复制下拉同步
        try {
          window.dispatchEvent(new CustomEvent('kaomoji:sort-mode-changed', { detail: { mode, source: 'settings' } }));
        } catch (e) {}
      };
      $('#sp_kaomojiSort').off('change').on('change', applyKaomojiSortImmediately);
      (function initPostExpandModeSelect() {
          const sel = document.getElementById('sp_postExpandAllMode');
          if (!sel) return;
          sel.value = (SettingPanel.state && SettingPanel.state.enablePostExpandAll) ? 'expand' : 'collapse';
          sel.addEventListener('change', (e) => {
              e.stopPropagation();
              const nextState = (sel.value || 'expand') === 'expand';
              SettingPanel.state.enablePostExpandAll = nextState;
              const items = document.querySelectorAll('.h-threads-item-index');
              let anchor = null;
              for (const item of items) {
                const rect = item.getBoundingClientRect();
                if (rect.top >= 0) { anchor = item; break; }
              }
              if (!anchor && items.length) anchor = items[items.length - 1];
              const anchorTopBefore = anchor ? anchor.getBoundingClientRect().top : null;
              items.forEach(item => {
                const toggleBtn = item.querySelector('.h-threads-info .js-toggle-mode');
                if (!toggleBtn) return;
                const expanded = item.classList.contains('expanded');
                if (nextState && !expanded) {
                  toggleBtn.click();
                } else if (!nextState && expanded) {
                  toggleBtn.click();
                }
              });
              if (!nextState && anchor && anchorTopBefore !== null) {
                requestAnimationFrame(() => {
                  const anchorTopAfter = anchor.getBoundingClientRect().top;
                  const delta = anchorTopAfter - anchorTopBefore;
                  window.scrollBy({ top: delta, behavior: 'instant' });
                });
              }
              try { GM_setValue(SettingPanel.key, SettingPanel.state); } catch (err) {}
              toast(nextState ? '已展开长串' : '已折叠长串');
          });
      })();
      (function initPostAfterActionSelect() {
          const sel = document.getElementById('sp_postAfterAction');
          if (!sel) return;
          sel.value = (SettingPanel.state && SettingPanel.state.postAfterAction) || 'jump';
          sel.addEventListener('change', (e) => {
              e.stopPropagation();
              const action = (sel.value || 'jump') === 'jump' ? 'jump' : 'refresh';
              SettingPanel.state.postAfterAction = action;
              try { GM_setValue(SettingPanel.key, SettingPanel.state); } catch (err) {}
              toast(action === 'jump' ? '已切换为:发串后新标签页打开' : '已切换为:发串后刷新页面回顶部');
          });
      })();
      (function initDraftModeSelect() {
          const sel = document.getElementById('sp_enableDraftMode');
          if (!sel) return;
          sel.value = (SettingPanel.state && SettingPanel.state.enableDraft) ? 'off' : 'on';
          sel.addEventListener('change', (e) => {
            e.stopPropagation();
            const enabled = (sel.value || 'off') === 'off';
            SettingPanel.state.enableDraft = enabled;
            try { GM_setValue(SettingPanel.key, SettingPanel.state); } catch (err) {}
            if (!enabled) {
              deleteAllDraftsSafe();
              toast('已清除缓存草稿并关闭草稿功能');
            } else {
              try { migrateLegacyDraftIfNeeded(); } catch (err) {}
              try { if (typeof 载入编辑 === 'function') 载入编辑(); } catch (err) {}
              try { if (typeof 注册自动保存编辑 === 'function') 注册自动保存编辑(); } catch (err) {}
              toast('已开启草稿缓存');
            }
          });
      })();
      (function initTimeDisplayModeSelect() {
          const sel = document.getElementById('sp_timeDisplayMode');
          if (!sel) return;
          sel.value = (SettingPanel.state && SettingPanel.state.timeDisplayMode === 'exact') ? 'exact' : 'relative';
          sel.addEventListener('change', (e) => {
            e.stopPropagation();
            const mode = (sel.value === 'exact') ? 'exact' : 'relative';
            SettingPanel.state.timeDisplayMode = mode;
            try { GM_setValue(SettingPanel.key, SettingPanel.state); } catch (err) {}
            try { if (typeof window.__xdexApplyTimeDisplayMode === 'function') window.__xdexApplyTimeDisplayMode(document); } catch (err) {}
            toast(mode === 'exact' ? '已切换为精确时间' : '已切换为相对时间');
          });
      })();
      // 标记:新增组输入
      $('#btn_group_marked').off('click').on('click', e=>{
        e.stopPropagation();
        const nextIndex = $('#marked-inputs-container .marked-row').length + 1;
        $('#marked-inputs-container').append(
          spUi('buildCookieGroupTwoFieldRowHtml', 'marked', nextIndex)
        ).find('.marked-desc-input').last().focus();
      });
      // 屏蔽:新增组输入
      $('#btn_group_blocked').off('click').on('click', e=>{
        e.stopPropagation();
        const nextIndex = $('#blocked-inputs-container .blocked-row').length + 1;
        $('#blocked-inputs-container').append(
          spUi('buildCookieGroupTwoFieldRowHtml', 'blocked', nextIndex)
        ).find('.blocked-desc-input').last().focus();
      });
      $('#btn_group_threadCookieWhitelist').off('click').on('click', e=>{
        e.stopPropagation();
        const nextIndex = $('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').length + 1;
        $('#thread-cookie-whitelist-inputs-container').append(spUi('buildThreadCookieWhitelistRowHtml', nextIndex));
        $('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').last().find('.thread-cookie-whitelist-desc-input').focus();
      });
      $('#btn_group_blockedKeywords').off('click').on('click', e=>{
        e.stopPropagation();
        const nextIndex = $('#blocked-keyword-inputs-container .blocked-keyword-row').length + 1;
        $('#blocked-keyword-inputs-container').append(spUi('buildBlockedKeywordGroupRowHtml', nextIndex));
        $('#blocked-keyword-inputs-container .blocked-keyword-row').last().find('.blocked-keyword-input').focus();
      });
      $('#btn_group_favoriteThreads').off('click').on('click', e=>{
        e.stopPropagation();
        const nextIndex = $('#favorite-thread-inputs-container .favorite-thread-row').length + 1;
        $('#favorite-thread-inputs-container').append(spUi('buildFavoriteThreadRowHtml', nextIndex));
        $('#favorite-thread-inputs-container .favorite-thread-row').last().find('.favorite-thread-desc-input').focus();
      });
      $('#btn_group_subscriptionFeeds').off('click').on('click', e=>{
        e.stopPropagation();
        const nextIndex = $('#subscription-feed-inputs-container .subscription-feed-row').length + 1;
        $('#subscription-feed-inputs-container').append(spUi('buildSubscriptionFeedRowHtml', nextIndex));
        $('#subscription-feed-inputs-container .subscription-feed-row').last().find('.subscription-feed-desc-input').focus();
      });
      // 「我的订阅」旁的齿轮:滚动到订阅号折叠区并展开(默认订阅号在行内设置)
      $('#sp_openDefaultSubscriptionFeed').off('click').on('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const $fold = $('#subscription-feed-inputs-container').closest('.sp_fold');
        if (!$fold.length) return;
        const $body = $fold.find('.sp_fold_body');
        if (!$body.is(':visible')) $fold.find('.sp_fold_head').first().trigger('click');
        setTimeout(() => {
          try { $fold[0].scrollIntoView({ block: 'center', behavior: 'smooth' }); }
          catch (err) { try { $fold[0].scrollIntoView(); } catch (e2) {} }
        }, 80);
      });
      const saveMarkedGroups = ({ fromDelete = false } = {}) => {
        const parsed = collectMarkedGroupsFromPanel();
        if (!parsed) return false;
        this.state.markedGroups = parsed;
        GM_setValue(this.key, this.state);
        this.syncInputs();
        toast(fromDelete ? '已删除标记分组' : '标记分组已保存');
        refreshFilterDisplay(this.state);
        return true;
      };
      const saveBlockedGroups = ({ fromDelete = false } = {}) => {
        const parsed = [];
        let valid = true;
        $('#blocked-inputs-container .blocked-row').each((idx, el)=>{
          const $row = $(el);
          const desc = ($row.find('.blocked-desc-input').val() || '').trim();
          const cookies = Utils.strToList(($row.find('.blocked-cookies-input').val() || '').trim());
          if (!desc && !cookies.length) return;
          if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid=false; return false; }
          if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid=false; return false; }
          if (cookies.some(id=>!Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid=false; return false; }
          parsed.push({ desc, cookies });
        });
        if (!valid) return false;
        this.state.blockedCookies = parsed;
        GM_setValue(this.key, this.state);
        this.syncInputs();
        toast(fromDelete ? '已删除屏蔽分组' : '屏蔽分组已保存');
        refreshFilterDisplay(this.state);
        return true;
      };
      const saveThreadCookieWhitelistGroups = ({ fromDelete = false } = {}) => {
        const parsed = [];
        let valid = true;
        $('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').each((idx, el) => {
          const $row = $(el);
          const desc = ($row.find('.thread-cookie-whitelist-desc-input').val() || '').trim();
          const threads = Utils.strToList(($row.find('.thread-cookie-whitelist-threads-input').val() || '').trim());
          const cookies = Utils.strToList(($row.find('.thread-cookie-whitelist-cookies-input').val() || '').trim());
          if (!desc && !threads.length && !cookies.length) return;
          if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid = false; return false; }
          if (!threads.length) { toast(`第${idx + 1}条未指定串号`); valid = false; return false; }
          if (threads.some(id => !spData('isValidThreadId', id))) { toast(`第${idx + 1}条存在不合法串号`); valid = false; return false; }
          if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid = false; return false; }
          if (cookies.some(id => !Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid = false; return false; }
          parsed.push({ desc, threads, cookies, rowIndex: idx + 1 });
        });
        if (!valid) return false;
        const { groups, mergeEvents } = spData('mergeThreadCookieWhitelistGroups', parsed);
        this.state.threadCookieWhitelistGroups = groups;
        GM_setValue(this.key, this.state);
        mergeEvents.forEach((event) => {
          if (event.desc) {
            toast(`第${event.rowIndex}条(${event.desc})中的串号 ${event.threadId} 已与现有只看规则合并`);
          } else {
            toast(`第${event.rowIndex}条的串号 ${event.threadId}(${event.cookies.join(',')})已与现有只看规则合并`);
          }
        });
        this.syncInputs();
        toast(fromDelete ? '已删除只看饼干分组' : '只看饼干分组已保存');
        refreshFilterDisplay(this.state);
        return true;
      };
      const collectBlockedKeywordGroupsFromPanel = () => {
        const parsed = [];
        let valid = true;
        $('#blocked-keyword-inputs-container .blocked-keyword-row').each((idx, el)=>{
          const $row = $(el);
          const rawValue = ($row.find('.blocked-keyword-input').val() || '').trim();
          const regex = !!$row.find('.blocked-keyword-regex').prop('checked');
          if (!rawValue) return;
          if (regex) {
            if (!spData('compileBlockedKeywordRegex', rawValue)) {
              toast(`第${idx + 1}组正则无效,请检查表达式`);
              valid = false;
              return false;
            }
            parsed.push({ value: rawValue, regex: true });
            return;
          }
          if (Utils.strToList(rawValue).length) parsed.push({ value: rawValue, regex: false });
        });
        return valid ? parsed : null;
      };
      const saveBlockedKeywordGroups = ({ fromDelete = false, fromToggle = false, toggleIndex = 0, toggleOn = false } = {}) => {
        const parsed = collectBlockedKeywordGroupsFromPanel();
        if (!parsed) return false;
        this.state.blockedKeywords = parsed;
        GM_setValue(this.key, this.state);
        // 开关切换实时保存时避免 syncInputs 整表重绘打断当前行焦点
        if (!fromToggle) this.syncInputs();
        if (fromDelete) toast('已删除关键词分组');
        else if (fromToggle) toast(`第${toggleIndex}组${toggleOn ? '开启' : '关闭'}正则`);
        else toast('屏蔽关键词已保存');
        refreshFilterDisplay(this.state);
        return true;
      };
      const saveFavoriteThreads = ({ fromDelete = false } = {}) => {
        const parsed = spUi('collectFavoriteThreadsFromPanel');
        if (!parsed) return false;
        this.state.favoriteThreads = parsed;
        GM_setValue(this.key, this.state);
        this.syncInputs();
        renderFavoriteThreadsMenu();
        toast(fromDelete ? '已删除常用串' : '常用串已保存');
        return true;
      };
      $('#marked-inputs-container').off('click', '.marked-delete').on('click', '.marked-delete', (e) => {
        e.preventDefault();
        e.stopPropagation();
        $(e.currentTarget).closest('.marked-row').remove();
        saveMarkedGroups({ fromDelete: true });
        return false;
      });
      $('#blocked-inputs-container').off('click', '.blocked-delete').on('click', '.blocked-delete', (e) => {
        e.preventDefault();
        e.stopPropagation();
        $(e.currentTarget).closest('.blocked-row').remove();
        saveBlockedGroups({ fromDelete: true });
        return false;
      });
      $('#thread-cookie-whitelist-inputs-container').off('click', '.thread-cookie-whitelist-delete').on('click', '.thread-cookie-whitelist-delete', (e) => {
        e.preventDefault();
        e.stopPropagation();
        $(e.currentTarget).closest('.thread-cookie-whitelist-row').remove();
        saveThreadCookieWhitelistGroups({ fromDelete: true });
        return false;
      });
      $('#blocked-keyword-inputs-container').off('click', '.blocked-keyword-delete').on('click', '.blocked-keyword-delete', (e) => {
        e.preventDefault();
        e.stopPropagation();
        $(e.currentTarget).closest('.blocked-keyword-row').remove();
        saveBlockedKeywordGroups({ fromDelete: true });
        return false;
      });
            // 正则开关:切换 placeholder,并实时保存 + 刷新过滤(无需点保存)
      $('#blocked-keyword-inputs-container').off('change.blockedKeywordRegex', '.blocked-keyword-regex').on('change.blockedKeywordRegex', '.blocked-keyword-regex', function () {
        const $sw = $(this);
        const $row = $sw.closest('.blocked-keyword-row');
        const on = !!$sw.prop('checked');
        const toggleIndex = $('#blocked-keyword-inputs-container .blocked-keyword-row').index($row) + 1;
        $row.attr('data-regex', on ? '1' : '0');
        $row.find('.blocked-keyword-input').attr('placeholder', spUi('getBlockedKeywordPlaceholder', on));
        // 实时写入配置并 applyFilters;失败(如正则非法)则回滚开关
        if (!saveBlockedKeywordGroups({ fromToggle: true, toggleIndex, toggleOn: on })) {
          $sw.prop('checked', !on);
          $row.attr('data-regex', !on ? '1' : '0');
          $row.find('.blocked-keyword-input').attr('placeholder', spUi('getBlockedKeywordPlaceholder', !on));
        }
      });
$('#favorite-thread-inputs-container').off('click', '.favorite-thread-delete').on('click', '.favorite-thread-delete', (e) => {
        e.preventDefault();
        e.stopPropagation();
        $(e.currentTarget).closest('.favorite-thread-row').remove();
        saveFavoriteThreads({ fromDelete: true });
        return false;
      });
      // 标记:保存
      $('#btn_sp_marked').off('click').on('click', e=>{
        e.stopPropagation();
        saveMarkedGroups();
      });
      // 屏蔽:保存
      $('#btn_sp_blocked').off('click').on('click', e=>{
        e.stopPropagation();
        saveBlockedGroups();
      });
      $('#btn_sp_blockedKeywords').off('click').on('click', e=>{
        e.stopPropagation();
        saveBlockedKeywordGroups();
      });
      $('#btn_sp_favoriteThreads').off('click').on('click', e=>{
        e.stopPropagation();
        saveFavoriteThreads();
      });
      $('#subscription-feed-inputs-container').off('click', '.subscription-feed-delete').on('click', '.subscription-feed-delete', (e) => {
        e.preventDefault();
        e.stopPropagation();
        $(e.currentTarget).closest('.subscription-feed-row').remove();
        saveSubscriptionFeeds({ fromDelete: true });
        return false;
      });
      // 订阅号:保存
      const saveSubscriptionFeeds = ({ fromDelete = false } = {}) => {
        const parsed = spUi('collectSubscriptionFeedsFromPanel');
        if (!parsed) return false;
        this.state.subscriptionFeeds = parsed;
        // 默认订阅号指向的 uuid 已被删除/修改 → 清空默认(打时间戳,供跨端“新者胜”)
        const defUuid = String(this.state.defaultSubscriptionFeedUuid || '').trim();
        if (defUuid && !parsed.some((item) => String(item.uuid || '').trim() === defUuid)) {
          this.state.defaultSubscriptionFeedUuid = '';
          this.state.defaultSubscriptionFeedUpdatedAt = Date.now();
        }
        GM_setValue(this.key, this.state);
        this.syncInputs();
        toast(fromDelete ? '已删除订阅号' : '订阅号已保存');
        return true;
      };
      $('#btn_sp_subscriptionFeeds').off('click').on('click', e=>{
        e.stopPropagation();
        saveSubscriptionFeeds();
      });
      // 默认订阅号开关(每行一个,互斥;最多存在一个默认)
      $('#subscription-feed-inputs-container').off('change.subscriptionDefault', '.subscription-feed-default')
        .on('change.subscriptionDefault', '.subscription-feed-default', (e) => {
          const $sw = $(e.currentTarget);
          const $row = $sw.closest('.subscription-feed-row');
          const uuid = String($row.find('.subscription-feed-uuid-input').val() || '').trim();
          if ($sw.is(':checked')) {
            if (!uuid) {
              $sw.prop('checked', false);
              toast('请先填写订阅号');
              return;
            }
            const savedUuids = ((typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [])
              .map((item) => String(item.uuid || '').trim());
            if (!savedUuids.includes(uuid)) {
              $sw.prop('checked', false);
              toast('请先保存该订阅号,再设为默认');
              return;
            }
            setDefaultSubscriptionFeedUuid(uuid);
            const feed = (((typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds) || [])
              .find((item) => String(item.uuid || '').trim() === uuid);
            toast(`已将默认订阅号设为「${(feed && feed.desc) || uuid}」`);
          } else {
            if (getDefaultSubscriptionFeedUuid() === uuid) {
              setDefaultSubscriptionFeedUuid('');
              toast('已取消默认订阅号');
            }
          }
        });
      $('#btn_sp_threadCookieWhitelist').off('click').on('click', e=>{
        e.stopPropagation();
        saveThreadCookieWhitelistGroups();
      });
      // ===== 分组行拖拽排序注册(拖动结束即保存 + 写入 groupOrderMeta 供跨端传播;失败自动回滚) =====
      installRowDragSorting({ containerSelector: '#marked-inputs-container', rowSelector: '.marked-row', field: 'markedGroups', onSorted: () => saveMarkedGroups() });
      installRowDragSorting({ containerSelector: '#blocked-inputs-container', rowSelector: '.blocked-row', field: 'blockedCookies', onSorted: () => saveBlockedGroups() });
      installRowDragSorting({ containerSelector: '#thread-cookie-whitelist-inputs-container', rowSelector: '.thread-cookie-whitelist-row', field: 'threadCookieWhitelistGroups', onSorted: () => saveThreadCookieWhitelistGroups() });
      installRowDragSorting({ containerSelector: '#blocked-keyword-inputs-container', rowSelector: '.blocked-keyword-row', field: 'blockedKeywords', onSorted: () => saveBlockedKeywordGroups() });
      installRowDragSorting({ containerSelector: '#favorite-thread-inputs-container', rowSelector: '.favorite-thread-row', field: 'favoriteThreads', onSorted: () => saveFavoriteThreads() });
      installRowDragSorting({ containerSelector: '#subscription-feed-inputs-container', rowSelector: '.subscription-feed-row', field: 'subscriptionFeeds', onSorted: () => saveSubscriptionFeeds() });
      const closeMarkedColorPopovers = () => {
        $('#marked-inputs-container .marked-color-popover').hide();
      };
      const getMarkedRowDefaultColor = ($row) => {
        const rowIndex = Math.max($row.index(), 0);
        return XDex.data.markColors[rowIndex % XDex.data.markColors.length];
      };
      const updateMarkedRowSwatch = ($row, draftColor) => {
        const color = spData('normalizeHexColor', draftColor) || getMarkedRowDefaultColor($row);
        $row.find('.marked-color-swatch').css('background', color).attr('data-default-color', color);
        $row.find('.marked-color-preview').css('background', color);
      };
      const readMarkedColorInputValue = ($popover) => ($popover.find('.marked-color-input').val() || '').trim();
      const readMarkedColorInputAsHex = ($popover) => {
        const state = $popover.data('pickerState') || {};
        const rawValue = readMarkedColorInputValue($popover);
        if (!rawValue) return '';
        if ((state.format || 'hex') === 'rgb') {
          const rgb = spData('parseRgbColorString', rawValue);
          return rgb ? spData('rgbToHex', rgb) : null;
        }
        const normalized = spData('normalizeHexColor', rawValue);
        return normalized || null;
      };
      const setMarkedColorInputFromState = ($popover) => {
        const state = $popover.data('pickerState');
        if (!state) return;
        const inputValue = state.inputEmpty
          ? ''
          : (state.format === 'rgb' ? spData('formatRgbColor', spData('hexToRgb', state.hex)) : state.hex);
        $popover.find('.marked-color-input').val(inputValue);
      };
      const updateMarkedColorFormatButtons = ($popover) => {
        const state = $popover.data('pickerState') || {};
        $popover.find('.marked-color-format').each((_, el) => {
          const $btn = $(el);
          const active = $btn.data('format') === (state.format || 'hex');
          $btn.css({
            border: active ? '1px solid #7da6bf' : '1px solid #a98f7a',
            background: active ? '#66CCFF' : '#F0E0D6',
            color: active ? '#fff' : '#6f5d50',
          });
        });
      };
      const renderMarkedColorPicker = ($row) => {
        const $popover = $row.find('.marked-color-popover');
        const state = $popover.data('pickerState');
        if (!state) return;
        const displayHex = spData('normalizeHexColor', state.hex) || state.defaultHex;
        const displayHsv = spData('hexToHsv', displayHex);
        const hueHex = spData('hsvToHex', state.hsv.h, 1, 1);
        const $sv = $popover.find('.marked-color-sv');
        const $svThumb = $popover.find('.marked-color-sv-thumb');
        const $hueThumb = $popover.find('.marked-color-hue-thumb');
        $sv.css('background', `linear-gradient(to top, #000 0%, transparent 100%), linear-gradient(to right, #fff 0%, ${hueHex} 100%)`);
        $svThumb.css({ left: `${state.hsv.s * 100}%`, top: `${(1 - state.hsv.v) * 100}%` });
        $hueThumb.css('left', `${(state.hsv.h / 360) * 100}%`);
        $popover.find('.marked-color-preview').css('background', displayHex);
        $popover.find('.marked-color-status').text(state.inputEmpty ? `默认色 ${state.defaultHex}` : displayHex);
        $popover.find('.marked-color-default-hint').text(`默认 ${state.defaultHex}`);
        setMarkedColorInputFromState($popover);
        updateMarkedColorFormatButtons($popover);
        updateMarkedRowSwatch($row, state.inputEmpty ? '' : displayHex);
      };
      const setMarkedColorPickerHex = ($row, nextHex, options = {}) => {
        const $popover = $row.find('.marked-color-popover');
        const state = $popover.data('pickerState');
        if (!state) return;
        const normalized = spData('normalizeHexColor', nextHex);
        const allowEmpty = !!options.allowEmpty;
        if (!normalized && !allowEmpty) return;
        state.inputEmpty = !normalized;
        state.hex = normalized || state.defaultHex;
        state.hsv = spData('hexToHsv', state.hex);
        $popover.data('pickerState', state);
        renderMarkedColorPicker($row);
      };
      const setMarkedColorPickerFormat = ($row, format) => {
        const $popover = $row.find('.marked-color-popover');
        const state = $popover.data('pickerState');
        if (!state) return;
        state.format = format === 'rgb' ? 'rgb' : 'hex';
        $popover.data('pickerState', state);
        renderMarkedColorPicker($row);
      };
      const updateMarkedColorPickerFromPointer = ($row, areaType, clientX, clientY) => {
        const $popover = $row.find('.marked-color-popover');
        const state = $popover.data('pickerState');
        if (!state) return;
        if (areaType === 'sv') {
          const rect = $popover.find('.marked-color-sv')[0].getBoundingClientRect();
          const s = spData('clampColorChannel', (clientX - rect.left) / rect.width, 0, 1);
          const v = spData('clampColorChannel', 1 - ((clientY - rect.top) / rect.height), 0, 1);
          state.hsv.s = s;
          state.hsv.v = v;
        } else {
          const rect = $popover.find('.marked-color-hue')[0].getBoundingClientRect();
          state.hsv.h = spData('clampColorChannel', ((clientX - rect.left) / rect.width) * 360, 0, 360);
          if (state.hsv.h === 360) state.hsv.h = 359.999;
        }
        state.inputEmpty = false;
        state.hex = spData('hsvToHex', state.hsv.h, state.hsv.s, state.hsv.v);
        $popover.data('pickerState', state);
        renderMarkedColorPicker($row);
      };
      const positionMarkedColorPopover = ($row) => {
        const $cell = $row.find('.marked-color-cell');
        const $popover = $row.find('.marked-color-popover');
        if (!$cell.length || !$popover.length) return;
        $popover.css({ top: '', bottom: '' });
        const cellRect = $cell[0].getBoundingClientRect();
        const popRect = $popover[0].getBoundingClientRect();
        const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
        const gap = 6;
        const spaceBelow = viewportHeight - cellRect.bottom - gap;
        const spaceAbove = cellRect.top - gap;
        const openAbove = spaceBelow < popRect.height && spaceAbove > spaceBelow;
        const popoverHeight = popRect.height;
        if (openAbove) {
          let offsetBottom = cellRect.height + gap;
          const topIfOpened = cellRect.bottom - offsetBottom - popoverHeight;
          if (topIfOpened < 4) {
            offsetBottom = Math.max(cellRect.bottom - popoverHeight - 4, gap);
          }
          $popover.css({ top: 'auto', bottom: `${offsetBottom}px` });
        } else {
          let offsetTop = cellRect.height + gap;
          const overflow = cellRect.top + offsetTop + popoverHeight - viewportHeight + 4;
          if (overflow > 0) {
            offsetTop = Math.max(offsetTop - overflow, -cellRect.top + 4);
          }
          $popover.css({ top: `${offsetTop}px`, bottom: 'auto' });
        }
      };
      const openMarkedColorPopover = ($row) => {
        const $popover = $row.find('.marked-color-popover');
        const storedColor = ($row.find('.marked-color-value').val() || '').trim();
        const defaultHex = getMarkedRowDefaultColor($row);
        const displayHex = spData('normalizeHexColor', storedColor) || defaultHex;
        $popover.data('pickerState', {
          format: 'hex',
          defaultHex,
          inputEmpty: !spData('normalizeHexColor', storedColor),
          hex: displayHex,
          hsv: spData('hexToHsv', displayHex),
        });
        renderMarkedColorPicker($row);
        $popover.show();
        positionMarkedColorPopover($row);
        $popover.find('.marked-color-input').trigger('focus').trigger('select');
      };
      const beginMarkedColorDrag = ($row, areaType, startEvent) => {
        const moveEvent = startEvent.type.indexOf('touch') === 0 ? 'touchmove.markedColorDrag' : 'mousemove.markedColorDrag';
        const endEvent = startEvent.type.indexOf('touch') === 0 ? 'touchend.markedColorDrag touchcancel.markedColorDrag' : 'mouseup.markedColorDrag';
        const getPoint = (evt) => {
          const touch = evt.originalEvent && evt.originalEvent.touches && evt.originalEvent.touches[0];
          const changedTouch = evt.originalEvent && evt.originalEvent.changedTouches && evt.originalEvent.changedTouches[0];
          return touch || changedTouch || evt;
        };
        const applyPointer = (evt) => {
          const point = getPoint(evt);
          updateMarkedColorPickerFromPointer($row, areaType, point.clientX, point.clientY);
        };
        applyPointer(startEvent);
        $(document).off('.markedColorDrag').on(moveEvent, (evt) => {
          evt.preventDefault();
          applyPointer(evt);
        }).on(endEvent, () => {
          $(document).off('.markedColorDrag');
        });
      };
      const collectMarkedGroupsFromPanel = () => {
        const parsed = [];
        let valid = true;
        // 与已保存分组按备注比对,仅在配色实际变化时刷新 colorAt(拖拽排序/改饼干不误触发)
        const prevGroups = (typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state && Array.isArray(SettingPanel.state.markedGroups))
          ? SettingPanel.state.markedGroups
          : [];
        $('#marked-inputs-container .marked-row').each((idx, el)=>{
          const $row = $(el);
          const desc = ($row.find('.marked-desc-input').val() || '').trim();
          const cookies = Utils.strToList(($row.find('.marked-cookies-input').val() || '').trim());
          const rawColor = ($row.find('.marked-color-value').val() || '').trim();
          if (!desc && !cookies.length) return;
          if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid=false; return false; }
          if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid=false; return false; }
          if (cookies.some(id=>!Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid=false; return false; }
          if (rawColor && !spData('isValidHexColor', rawColor)) { toast(`第${idx + 1}条颜色格式无效,应为 #RRGGBB`); valid=false; return false; }
          const color = spData('normalizeHexColor', rawColor);
          const prev = prevGroups.find((g) => String((g && g.desc) || '') === desc);
          const prevColor = prev ? spData('normalizeHexColor', prev.color) : '';
          const prevAt = prev ? Math.max(0, Number(prev.colorAt) || 0) : 0;
          // 配色未变 → 保留旧时间戳;变化(含清除)→ 打当前时间
          const colorAt = color === prevColor ? prevAt : Date.now();
          parsed.push({ desc, color, colorAt, cookies });
        });
        return valid ? parsed : null;
      };
      $(document).off('click.markedColorPopover').on('click.markedColorPopover', (e) => {
        if ($(e.target).closest('#marked-inputs-container .marked-color-cell').length) return;
        closeMarkedColorPopovers();
      });
      $('#marked-inputs-container').off('click', '.marked-color-swatch').on('click', '.marked-color-swatch', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const $row = $(e.currentTarget).closest('.marked-row');
        const $popover = $row.find('.marked-color-popover');
        const shouldOpen = !$popover.is(':visible');
        closeMarkedColorPopovers();
        if (!shouldOpen) return false;
        openMarkedColorPopover($row);
        return false;
      });
      $('#marked-inputs-container').off('input', '.marked-color-input').on('input', '.marked-color-input', (e) => {
        const $row = $(e.currentTarget).closest('.marked-row');
        const $popover = $row.find('.marked-color-popover');
        const normalized = readMarkedColorInputAsHex($popover);
        if (normalized === '') {
          setMarkedColorPickerHex($row, '', { allowEmpty: true });
          return;
        }
        if (normalized) setMarkedColorPickerHex($row, normalized);
      });
      $('#marked-inputs-container').off('blur', '.marked-color-input').on('blur', '.marked-color-input', (e) => {
        const $row = $(e.currentTarget).closest('.marked-row');
        const $popover = $row.find('.marked-color-popover');
        const parsedHex = readMarkedColorInputAsHex($popover);
        if (parsedHex === '') {
          setMarkedColorPickerHex($row, '', { allowEmpty: true });
          return;
        }
        if (parsedHex) {
          setMarkedColorPickerHex($row, parsedHex);
          return;
        }
        renderMarkedColorPicker($row);
      });
      $('#marked-inputs-container').off('click', '.marked-color-format').on('click', '.marked-color-format', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const $row = $(e.currentTarget).closest('.marked-row');
        setMarkedColorPickerFormat($row, $(e.currentTarget).data('format'));
        return false;
      });
      $('#marked-inputs-container').off('mousedown touchstart', '.marked-color-sv').on('mousedown touchstart', '.marked-color-sv', (e) => {
        e.preventDefault();
        e.stopPropagation();
        beginMarkedColorDrag($(e.currentTarget).closest('.marked-row'), 'sv', e);
        return false;
      });
      $('#marked-inputs-container').off('mousedown touchstart', '.marked-color-hue').on('mousedown touchstart', '.marked-color-hue', (e) => {
        e.preventDefault();
        e.stopPropagation();
        beginMarkedColorDrag($(e.currentTarget).closest('.marked-row'), 'hue', e);
        return false;
      });
      $('#marked-inputs-container').off('click', '.marked-color-clear').on('click', '.marked-color-clear', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const $row = $(e.currentTarget).closest('.marked-row');
        const $popover = $row.find('.marked-color-popover');
        $row.find('.marked-color-value').val('');
        if ($popover.is(':visible')) {
          setMarkedColorPickerHex($row, '', { allowEmpty: true });
          const state = $popover.data('pickerState');
          if (state) {
            state.format = 'hex';
            $popover.data('pickerState', state);
            renderMarkedColorPicker($row);
          }
        } else {
          updateMarkedRowSwatch($row, '');
        }
        $popover.hide();
        return false;
      });
      $('#marked-inputs-container').off('click', '.marked-color-save').on('click', '.marked-color-save', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const $row = $(e.currentTarget).closest('.marked-row');
        const $popover = $row.find('.marked-color-popover');
        const parsedHex = readMarkedColorInputAsHex($popover);
        if (parsedHex === null) {
          toast('颜色格式无效,应为 #RRGGBB 或 rgb(r, g, b)');
          $row.find('.marked-color-input').trigger('focus');
          return false;
        }
        const normalized = spData('normalizeHexColor', parsedHex);
        $row.find('.marked-color-value').val(normalized);
        if (normalized) {
          setMarkedColorPickerHex($row, normalized);
          setMarkedColorPickerFormat($row, 'hex');
        } else {
          setMarkedColorPickerHex($row, '', { allowEmpty: true });
        }
        updateMarkedRowSwatch($row, normalized);
        $popover.hide();
        // 直接复用「保存」(sp_save / #btn_sp_marked) 的那条路径:收集 → 持久化 → syncInputs
        // → refreshFilterDisplay(即时生效,无需刷新)。
        // 此前这里只改了 .marked-color-value 隐藏字段就关闭弹窗,颜色不会落盘。
        saveMarkedGroups();
        return false;
      });
      // ========== 导入/导出配置 ==========
      function parseJSONC(str) {
        // 逐字符解析:只在字符串外部去掉 // 注释和尾随逗号(不清理零宽字符,保留用户数据完整性)
        let out = '', inStr = false, esc = false;
        for (let i = 0; i < str.length; i++) {
          const ch = str[i];
          if (inStr) {
            out += ch;
            if (esc) { esc = false; }
            else if (ch === '\\') { esc = true; }
            else if (ch === '"') { inStr = false; }
          } else {
            if (ch === '"') { inStr = true; out += ch; }
            else if (ch === '/' && str[i + 1] === '/') {
              // 跳过行注释
              while (i < str.length && str[i] !== '\n') i++;
              out += '\n';
            }
            else if (ch === ',' && /^\s*[}\]]/.test(str.slice(i + 1))) {
              // 尾随逗号:后面紧跟 } 或 ](允许空白),跳过逗号
              continue;
            }
            else { out += ch; }
          }
        }
        return JSON.parse(out);
      }
      // 固定开启项:UI 中为 disabled + checked,无需导入/导出
      const FIXED_KEYS = new Set([
        'enableImageHideMode',   // 固定启用(applyImageHideMode 仍可导出)
        'interceptReplyForm',    // 拦截回复中间页
        'updateReplyNumbers',    // 当页回复编号
        'replaceRightSidebar',   // 扩展坞增强
        'kaomojiEnhancer',       // 颜文字拓展(kaomojiSort 仍可导出)
        'highlightPO',           // 标记Po主
        'enhancePostFormLayout', // 发串UI调整
        'applyFilters',          // 标记/屏蔽-饼干/关键词
        'enhanceIsland',         // 增强X岛匿名版
        'enablePostExpand',      // 展开板块页长串
        'searchServiceBy4sY',    // 野生搜索酱
      ]);
      // 清理字符串值中的零宽/不可见控制字符
      function sanitizeValue(val) {
        if (typeof val === 'string') {
          return val.replace(/[\u200B\u200C\u200D\uFEFF\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069]/g, '');
        }
        if (Array.isArray(val)) return val.map(sanitizeValue);
        if (val && typeof val === 'object') {
          const o = {};
          for (const [k2, v2] of Object.entries(val)) o[k2] = sanitizeValue(v2);
          return o;
        }
        return val;
      }
      // === 使用数据导入导出 ===
      const FULL_EXPORT_SCHEMA_VERSION = 1;
      const FULL_IMPORT_DIRECT_OVERRIDE_KEYS = [
        'enableCookieSwitch', 'enableCookieConfirm', 'disableWatermark', 'enablePaginationDuplication',
        'updatePreviewCookie', 'hideEmptyTitleEmail', 'enableExternalImagePreview',
        'enableAutoCookieRefresh', 'enableAutoCookieRefreshToast',
        'interceptReplyFormUnvcode', 'interceptReplyFormU200B',
        'interceptReplyFormAutoCompress', 'enableSeamlessPaging',
        'enableAutoSeamlessPaging', 'enableHDImageAndLayoutFix',
        'enableLinkBlank', 'enableAutoUrlLinkify', 'enableQuotePreview',
        'enableUpdateCheck', 'enableImageContextMenu', 'enableImageHideMode',
        'applyImageHideMode', 'imageViewerSeparators', 'enableDraft', 'timeDisplayMode',
        'extendQuote', 'kaomojiSort', 'toggleSidebar',
        'threadCookieWhitelistDisplayMode', 'poAnnotationSideDisplayMode',
        'replyModeDefault', 'replyExtraDefault', 'blockDisplayMode',
        'postAfterAction', 'enablePostExpandAll', 'dockDisplayMode', 'disableAutoQuote',
        'enableCustomDarkTheme'
      ];
      function mergeFavoriteThreads(localItems, importedItems) {
        const local = Array.isArray(localItems) ? spData('normalizeFavoriteThreads', localItems) : [];
        const imported = Array.isArray(importedItems) ? spData('normalizeFavoriteThreads', importedItems) : [];
        const seen = new Map();
        local.forEach((item) => {
          const key = String(item.threadId || '').trim();
          if (key) seen.set(key, item);
        });
        imported.forEach((item) => {
          const key = String(item.threadId || '').trim();
          if (!key) return;
          if (!seen.has(key)) {
            seen.set(key, item);
          } else {
            const existing = seen.get(key);
            if (!existing.desc && item.desc) existing.desc = item.desc;
          }
        });
        return Array.from(seen.values());
      }
      function mergeSubscriptionFeeds(localItems, importedItems) {
        const local = Array.isArray(localItems) ? localItems : [];
        const imported = Array.isArray(importedItems) ? importedItems : [];
        const seen = new Map();
        local.forEach((item) => {
          const key = String(item.uuid || '').trim();
          if (key) seen.set(key, item);
        });
        imported.forEach((item) => {
          const key = String(item.uuid || '').trim();
          if (!key) return;
          if (!seen.has(key)) {
            seen.set(key, item);
          } else {
            const existing = seen.get(key);
            if (!existing.desc && item.desc) existing.desc = item.desc;
          }
        });
        return Array.from(seen.values());
      }
      function mergeMarkedGroups(localGroups, importedGroups) {
        const local = Array.isArray(localGroups) ? localGroups : [];
        const imported = Array.isArray(importedGroups) ? importedGroups : [];
        const result = [];
        const usedImported = new Set();
        local.forEach((localGroup) => {
          const localDesc = localGroup.desc || localGroup.name || '';
          let matched = false;
          for (let i = 0; i < imported.length; i++) {
            if (usedImported.has(i)) continue;
            const impGroup = imported[i];
            const impDesc = impGroup.desc || impGroup.name || '';
            if (localDesc && impDesc && localDesc === impDesc) {
              matched = true;
              usedImported.add(i);
              const localCookies = Array.isArray(localGroup) ? localGroup : (localGroup.cookies || []);
              const impCookies = Array.isArray(impGroup) ? impGroup : (impGroup.cookies || []);
              const mergedCookies = Array.from(new Set([...localCookies, ...impCookies]));
              if (Array.isArray(localGroup) && !localGroup.desc) {
                result.push(mergedCookies);
              } else {
                const mergedGroup = { ...localGroup, ...impGroup, desc: impDesc || localDesc, cookies: mergedCookies };
                // 配色单独判定(spread 会让远端 color 无条件覆盖,包括用空串清空本地自定义色)
                const colorState = resolveMarkedGroupColorState(localGroup, impGroup);
                if (colorState) {
                  mergedGroup.color = colorState.color;
                  mergedGroup.colorAt = colorState.colorAt;
                }
                result.push(mergedGroup);
              }
              break;
            }
          }
          if (!matched) result.push(localGroup);
        });
        imported.forEach((impGroup, i) => { if (!usedImported.has(i)) result.push(impGroup); });
        return result;
      }
      function mergeBlockedCookies(localGroups, importedGroups) {
        return mergeMarkedGroups(localGroups, importedGroups);
      }
      function mergeWhitelistGroupsForImport(localGroups, importedGroups) {
        const combined = [...(Array.isArray(localGroups) ? localGroups : []), ...(Array.isArray(importedGroups) ? importedGroups : [])];
        return spData('mergeThreadCookieWhitelistGroups', combined).groups;
      }
      function mergeBlockedKeywords(localValue, importedValue) {
        const localGroups = spData('normalizeBlockedKeywordGroups', localValue);
        const importedGroups = spData('normalizeBlockedKeywordGroups', importedValue);
        const allLocalKws = new Set(spData('flattenBlockedKeywords', localGroups));
        const localRegexKeys = new Set(
          localGroups.filter((g) => g.regex).map((g) => String(g.value || ''))
        );
        const result = localGroups.map((g) => ({ value: g.value, regex: !!g.regex }));
        importedGroups.forEach((impGroup) => {
          if (impGroup.regex) {
            const key = String(impGroup.value || '');
            if (key && !localRegexKeys.has(key)) {
              result.push({ value: impGroup.value, regex: true });
              localRegexKeys.add(key);
            }
            return;
          }
          const impKws = Utils.strToList(impGroup.value);
          if (!impKws.length) return;
          const hasNew = impKws.some((kw) => !allLocalKws.has(kw));
          if (hasNew) {
            result.push({ value: impGroup.value, regex: false });
            impKws.forEach((kw) => allLocalKws.add(kw));
          }
        });
        return result;
      }
      function mergeSettingsForFullImport(localSettings, importedSettings) {
        const result = Object.assign({}, localSettings);
        FULL_IMPORT_DIRECT_OVERRIDE_KEYS.forEach((key) => {
          if (key in importedSettings) result[key] = importedSettings[key];
        });
        result.favoriteThreads = mergeFavoriteThreads(localSettings.favoriteThreads, importedSettings.favoriteThreads);
        result.subscriptionFeeds = mergeSubscriptionFeeds(localSettings.subscriptionFeeds, importedSettings.subscriptionFeeds);
        result.blockedKeywords = mergeBlockedKeywords(localSettings.blockedKeywords, importedSettings.blockedKeywords);
        result.markedGroups = mergeMarkedGroups(localSettings.markedGroups, importedSettings.markedGroups);
        result.blockedCookies = mergeBlockedCookies(localSettings.blockedCookies, importedSettings.blockedCookies);
        result.threadCookieWhitelistGroups = mergeWhitelistGroupsForImport(localSettings.threadCookieWhitelistGroups, importedSettings.threadCookieWhitelistGroups);
        // 默认订阅号:按 updatedAt 新者胜(从未设置过则任一方有值即覆盖)
        const defFeed = mergeDefaultSubscriptionFeedSetting(localSettings, importedSettings);
        result.defaultSubscriptionFeedUuid = defFeed.uuid;
        result.defaultSubscriptionFeedUpdatedAt = defFeed.updatedAt;
        // 顺序传播:按 groupOrderMeta 的 at 新者胜重排六个分组(静默,不弹窗)
        mergeGroupOrdersInto(result, localSettings, importedSettings);
        return result;
      }
      // WebDAV「保留本地上传」专用:标量项以本地为准(result 以 localSettings 为底),
      // 复杂项(分组类)无论方向都合并,避免上传全量覆盖时丢失远端分组
      function mergeSettingsForWebdavUpload(localSettings, remoteSettings) {
        const result = Object.assign({}, localSettings);
        if (!remoteSettings || typeof remoteSettings !== 'object') return result;
        result.favoriteThreads = mergeFavoriteThreads(localSettings.favoriteThreads, remoteSettings.favoriteThreads);
        result.subscriptionFeeds = mergeSubscriptionFeeds(localSettings.subscriptionFeeds, remoteSettings.subscriptionFeeds);
        result.blockedKeywords = mergeBlockedKeywords(localSettings.blockedKeywords, remoteSettings.blockedKeywords);
        result.markedGroups = mergeMarkedGroups(localSettings.markedGroups, remoteSettings.markedGroups);
        result.blockedCookies = mergeBlockedCookies(localSettings.blockedCookies, remoteSettings.blockedCookies);
        result.threadCookieWhitelistGroups = mergeWhitelistGroupsForImport(localSettings.threadCookieWhitelistGroups, remoteSettings.threadCookieWhitelistGroups);
        // 默认订阅号:同 full-import,按 updatedAt 新者胜
        const defFeed = mergeDefaultSubscriptionFeedSetting(localSettings, remoteSettings);
        result.defaultSubscriptionFeedUuid = defFeed.uuid;
        result.defaultSubscriptionFeedUpdatedAt = defFeed.updatedAt;
        // 顺序传播:同 full-import,按 groupOrderMeta 的 at 新者胜重排
        mergeGroupOrdersInto(result, localSettings, remoteSettings);
        return result;
      }
      function mergeThreadHistoryStore(localStore, importedStore) {
        // 本地导入/导出合并(非 WebDAV):浏览次数直接累加;最新浏览时间/串内最远取较大值,首访取较早值
        const local = normalizeThreadHistoryStore(localStore);
        const imported = normalizeThreadHistoryStore(importedStore);
        const result = Object.assign({}, local);
        result.items = Object.assign({}, local.items);
        result.index = Object.assign({}, local.index);
        Object.keys(imported.items).forEach((key) => {
          const impItem = imported.items[key];
          if (!result.items[key]) {
            result.items[key] = impItem;
            result.index[key] = imported.index[key] || buildThreadHistoryIndexEntry(impItem);
          } else {
            const localItem = result.items[key];
            const impNewer = (Number(impItem.lastVisitedAt) || 0) >= (Number(localItem.lastVisitedAt) || 0);
            const newer = impNewer ? impItem : localItem;
            result.items[key] = {
              ...localItem,
              ...impItem,
              firstVisitedAt: Math.min(Number(localItem.firstVisitedAt) || Infinity, Number(impItem.firstVisitedAt) || Infinity),
              lastVisitedAt: Math.max(Number(localItem.lastVisitedAt) || 0, Number(impItem.lastVisitedAt) || 0),
              page: Math.max(Number(localItem.page) || 0, Number(impItem.page) || 0),
              maxVisitedPage: Math.max(Number(localItem.maxVisitedPage) || 0, Number(impItem.maxVisitedPage) || 0),
              visitCount: (Number(localItem.visitCount) || 0) + (Number(impItem.visitCount) || 0),
              lastScrollY: newer.lastScrollY != null ? newer.lastScrollY : (localItem.lastScrollY != null ? localItem.lastScrollY : impItem.lastScrollY),
              title: (newer.title || '').trim() ? newer.title : (localItem.title || impItem.title),
              name: (newer.name || '').trim() ? newer.name : (localItem.name || impItem.name),
            };
            result.index[key] = buildThreadHistoryIndexEntry(result.items[key]); // index 为纯派生数据: 随合并后的 item 重建, 防止 lastVisitedAt 失步
          }
        });
        result.order = Object.keys(result.items)
          .sort((a, b) => (Number(result.items[b].lastVisitedAt) || 0) - (Number(result.items[a].lastVisitedAt) || 0));
        return result;
      }
      // WebDAV 浏览历史合并:基于“各端上次参与同步时的基线”计算独立贡献,避免直接累加导致多端计数翻倍
      // baseline.threadHistory[key].count = 该端上次参与 WebDAV 同步后本地 visitCount(从未同步过 → 0 → 全量计入)
      function mergeThreadHistoryStoreWebdav(localStore, remoteStore, baselines) {
        const local = normalizeThreadHistoryStore(localStore);
        const remote = normalizeThreadHistoryStore(remoteStore);
        const result = Object.assign({}, local);
        result.items = Object.assign({}, local.items);
        result.index = Object.assign({}, local.index);
        // ── 墓碑合并: deletedAt 取 max, record 取较新方, purged/revivedAt 双向传播 ──
        result.tombstones = Object.assign({}, local.tombstones || {});
        Object.keys(remote.tombstones || {}).forEach((tKey) => {
          const rt = remote.tombstones[tKey];
          const lt = result.tombstones[tKey];
          if (!lt) { result.tombstones[tKey] = rt; return; }
          const newerDeletedAt = Math.max(Number(rt.deletedAt) || 0, Number(lt.deletedAt) || 0);
          const newer = (Number(rt.deletedAt) || 0) >= (Number(lt.deletedAt) || 0) ? rt : lt;
          const purged = !!(rt.purged || lt.purged);
          const revivedAt = Math.max(Number(rt.revivedAt) || 0, Number(lt.revivedAt) || 0) || null;
          const mergedTomb = { deletedAt: newerDeletedAt, origin: newer.origin || 'local', purged, revivedAt };
          if (!purged && !revivedAt && newer.record) mergedTomb.record = newer.record;
          result.tombstones[tKey] = mergedTomb;
        });
        // 混跑防线+压制: 对存在本地墓碑(未复活)的 key, 过滤远端条目——
        // lastVisitedAt > deletedAt 视为删除后的新段(参与合并), 否则为旧版传回的删除前数据(丢弃)
        const effectiveRemoteItems = {};
        Object.keys(remote.items || {}).forEach((key) => {
          const t = result.tombstones[key];
          if (t && !t.revivedAt && ((Number(remote.items[key].lastVisitedAt) || 0) <= (Number(t.deletedAt) || 0))) return;
          effectiveRemoteItems[key] = remote.items[key];
        });
        const baselineMap = (baselines && baselines.threadHistory) || {};
        const keys = new Set(Object.keys(local.items).concat(Object.keys(effectiveRemoteItems)));
        const localTombsForItems = result.tombstones;
        keys.forEach((key) => {
          // 本端条目被远端墓碑压制: 条目早于删除时刻 → 应用删除(从合并结果移除)
          const ownTomb = localTombsForItems[key];
          if (ownTomb && !ownTomb.revivedAt && !effectiveRemoteItems[key] &&
              ((Number(result.items[key] && result.items[key].lastVisitedAt) || 0) <= (Number(ownTomb.deletedAt) || 0))) {
            delete result.items[key];
            delete result.index[key];
            return;
          }
          const localItem = local.items[key];
          const remoteItem = effectiveRemoteItems[key];
          const baseCount = Number((baselineMap[key] && baselineMap[key].count) || 0);
          const localCount = Number(localItem && localItem.visitCount) || 0;
          const remoteCount = Number(remoteItem && remoteItem.visitCount) || 0;
          // 独立贡献 = 本地当前值 - 上次参与同步时的值(未同步过则全量);远端值 += 独立贡献
          const delta = Math.max(0, localCount - baseCount);
          const mergedCount = remoteCount + delta;
          let mergedItem = null;
          if (remoteItem && localItem) {
            const impNewer = (Number(remoteItem.lastVisitedAt) || 0) >= (Number(localItem.lastVisitedAt) || 0);
            const newer = impNewer ? remoteItem : localItem;
            // page = 最近查看页(允许回退): 双端语义取 lastVisitedAt 较新一端的 page, 不取 max(max 会抹掉本地的主动回翻)
            const mergedPage = Math.max(1, Number(newer.page) || 1);
            const mergedMaxVp = Math.max(Number(localItem.maxVisitedPage) || 0, Number(remoteItem.maxVisitedPage) || 0, mergedPage);
            // url/lastKnownPage 禁止被 ...remoteItem 的过期快照覆盖(曾产生 page=97/url=?page=86 的矛盾记录):
            // url 与 mergedPage 同步(最近查看页), lastKnownPage 只取 max —— 保证写入存储的记录字段自洽
            const mergedLastKp = Math.max(Number(localItem.lastKnownPage) || 0, Number(remoteItem.lastKnownPage) || 0, mergedMaxVp || 0);
            const mergedUrl = buildThreadHistoryPageUrl(
              localItem.mode || remoteItem.mode,
              localItem.threadId || remoteItem.threadId,
              mergedPage
            );
            mergedItem = {
              ...localItem,
              ...remoteItem,
              firstVisitedAt: Math.min(Number(localItem.firstVisitedAt) || Infinity, Number(remoteItem.firstVisitedAt) || Infinity),
              lastVisitedAt: Math.max(Number(localItem.lastVisitedAt) || 0, Number(remoteItem.lastVisitedAt) || 0),
              page: mergedPage,
              maxVisitedPage: mergedMaxVp,
              lastKnownPage: mergedLastKp,
              url: mergedUrl,
              visitCount: mergedCount,
              lastScrollY: newer.lastScrollY != null ? newer.lastScrollY : (localItem.lastScrollY != null ? localItem.lastScrollY : remoteItem.lastScrollY),
              title: (newer.title || '').trim() ? newer.title : (localItem.title || remoteItem.title),
              name: (newer.name || '').trim() ? newer.name : (localItem.name || remoteItem.name),
            };
          } else if (localItem) {
            // 远端无此 key(从未有过,或被其他端的裸上传覆盖)时,本地值是唯一真相。
            // 不能沿用 mergedCount(=0+本地−基线):已同步过的条目会被算成 0 而清零
            mergedItem = Object.assign({}, localItem, { visitCount: localCount });
          } else if (remoteItem) {
            mergedItem = Object.assign({}, remoteItem, { visitCount: mergedCount });
          }
          if (mergedItem) {
            result.items[key] = mergedItem;
            result.index[key] = buildThreadHistoryIndexEntry(mergedItem); // index 为纯派生数据: 必须随合并后的 item 重建, 沿用旧值会导致 lastVisitedAt 失步、排序错乱
          }
        });
        result.order = Object.keys(result.items)
          .sort((a, b) => (Number(result.items[b].lastVisitedAt) || 0) - (Number(result.items[a].lastVisitedAt) || 0));
        return result;
      }
      function getWebdavHistoryBaselines() {
        try {
          const v = GM_getValue('xdex_webdav_history_baselines', null);
          return (v && typeof v === 'object') ? v : {};
        } catch (e) { return {}; }
      }
      function saveWebdavHistoryBaselinesFromStore(store, existingBaselines) {
        const baselines = Object.assign({}, existingBaselines || getWebdavHistoryBaselines());
        baselines.threadHistory = Object.assign({}, baselines.threadHistory || {});
        Object.keys(store.items || {}).forEach((key) => {
          baselines.threadHistory[key] = { count: Number(store.items[key].visitCount) || 0, at: Date.now() };
        });
        try { GM_setValue('xdex_webdav_history_baselines', baselines); } catch (e) {}
        return baselines;
      }
      function postHistoryMatchKey(item) {
        if (!item) return '';
        // 已确认记录优先用服务端 No.(跨端稳定);未确认退回 type+resto+内容指纹
        const postId = String(item.postId || item.id || '').trim();
        if (postId) return 'id:' + postId;
        const type = normalizePostHistoryType(item.type);
        const resto = String(item.resto || (type === 'reply' ? item.threadId : '') || '').trim();
        const hash = item.contentHash || hashPostHistoryText(item.contentText || item.contentRaw || '');
        return 'fp:' + type + ':' + resto + ':' + hash;
      }
      function mergePostHistoryStore(localStore, importedStore) {
        const local = normalizePostHistoryStore(localStore);
        const imported = normalizePostHistoryStore(importedStore);
        const result = Object.assign({}, local);
        result.items = Object.assign({}, local.items);
        // ── 墓碑合并: deletedAt 取 max, record 取较新方, purged/revivedAt 双向传播 ──
        result.tombstones = Object.assign({}, local.tombstones || {});
        Object.keys(imported.tombstones || {}).forEach((tKey) => {
          const rt = imported.tombstones[tKey];
          const lt = result.tombstones[tKey];
          if (!lt) { result.tombstones[tKey] = rt; return; }
          const newerDeletedAt = Math.max(Number(rt.deletedAt) || 0, Number(lt.deletedAt) || 0);
          const newer = (Number(rt.deletedAt) || 0) >= (Number(lt.deletedAt) || 0) ? rt : lt;
          const purged = !!(rt.purged || lt.purged);
          const revivedAt = Math.max(Number(rt.revivedAt) || 0, Number(lt.revivedAt) || 0) || null;
          const mergedTomb = { deletedAt: newerDeletedAt, origin: newer.origin || 'local', purged, revivedAt };
          if (!purged && !revivedAt && newer.record) mergedTomb.record = newer.record;
          result.tombstones[tKey] = mergedTomb;
        });
        // 混跑防线: 被本地墓碑(未复活)压制且早于删除时刻的导入条目直接丢弃
        Object.keys(imported.items).forEach((impKey) => {
          const mk = postHistoryMatchKey(imported.items[impKey]);
          if (!mk) return;
          const t = result.tombstones[mk];
          if (t && !t.revivedAt && ((Number(imported.items[impKey].submittedAt) || Infinity) <= (Number(t.deletedAt) || 0))) {
            imported.items[impKey] = null;
          }
        });
        // 本端条目被远端墓碑压制: 早于删除时刻 → 应用删除
        Object.keys(result.items).forEach((localKey) => {
          const mk = postHistoryMatchKey(result.items[localKey]);
          if (!mk) return;
          const t = result.tombstones[mk];
          if (t && !t.revivedAt && ((Number(result.items[localKey].submittedAt) || Infinity) <= (Number(t.deletedAt) || 0))) {
            delete result.items[localKey];
          }
        });
        const STATUS_PRIORITY = { confirmed: 3, unconfirmed: 2, pending: 1, failed: 0 };
        // 归一化匹配索引:避免两端 localId 不同导致同一帖子重复
        const matchIndex = new Map();
        Object.keys(result.items).forEach((key) => {
          const mk = postHistoryMatchKey(result.items[key]);
          if (mk) matchIndex.set(mk, key);
        });
        Object.keys(imported.items).forEach((key) => {
          const impItem = imported.items[key];
          if (!impItem) return;   // 已被墓碑混跑防线丢弃
          const mk = postHistoryMatchKey(impItem);
          const matchKey = mk ? matchIndex.get(mk) : null;
          if (!matchKey || !result.items[matchKey]) {
            result.items[key] = impItem;
            if (mk && key) matchIndex.set(mk, key);
            return;
          }
          const localItem = result.items[matchKey];
          const localStatus = STATUS_PRIORITY[localItem.status] || 0;
          const impStatus = STATUS_PRIORITY[impItem.status] || 0;
          result.items[matchKey] = {
            ...localItem,
            ...impItem,
            id: String(impItem.id || localItem.id || ''),
            postId: String(impItem.postId || localItem.postId || impItem.id || localItem.id || ''),
            threadId: String(impItem.threadId || localItem.threadId || ''),
            resto: String(impItem.resto || localItem.resto || ''),
            status: impStatus >= localStatus ? impItem.status : localItem.status,
            page: Math.max(Number(localItem.page) || 0, Number(impItem.page) || 0),
            submittedAt: Math.min(Number(localItem.submittedAt) || Infinity, Number(impItem.submittedAt) || Infinity),
            contentText: impItem.contentText || localItem.contentText,
            forumName: impItem.forumName || localItem.forumName,
          };
        });
        result.order = Object.keys(result.items)
          .sort((a, b) => (Number(result.items[b].submittedAt) || 0) - (Number(result.items[a].submittedAt) || 0));
        return result;
      }
      function normalizeDraftExportText(text) {
        return String(text || '')
          .replace(/^\uFEFF/, '')
          .replace(/[\u200B\u200C\u200D\uFEFF\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069]/g, '')
          .trim();
      }
      function collectDraftsForExport() {
        const registry = getDraftRegistry();
        const items = {};
        registry.forEach((key) => {
          const canonical = key.indexOf('xdex_draft:') === 0 ? key : ('xdex_draft:' + key);
          const pathname = canonical.slice('xdex_draft:'.length);
          const raw = readDraftValue(pathname);
          const clean = normalizeDraftExportText(raw);
          if (clean) items[canonical] = raw;
        });
        return { registry: Object.keys(items), items, deletions: getDraftDeletionStore() };
      }
      function applyDraftsFromImport(drafts) {
        if (!drafts || typeof drafts !== 'object') return { imported: 0, overwritten: 0 };
        const items = drafts.items || {};
        // 远端账本仅用于本轮导入裁决, 不回灌本地: 否则"重新编辑翻案"会被旧账本抹杀
        const localDeletionStore = getDraftDeletionStore();
        const remoteDeletions = (drafts.deletions && typeof drafts.deletions === 'object') ? drafts.deletions : {};
        // 本地草稿命中"本地账本"(本端删除且未翻案) → 删除本地
        Object.keys(localDeletionStore).forEach((ck) => {
          if (items[ck] !== undefined) return;   // 远端仍有内容 = 重新编辑翻案, 保留
          let v = '';
          try { v = GM_getValue(ck, ''); } catch (_) {}
          if (typeof v === 'string' && v !== '') {
            try { deleteDraftSafe(ck); } catch (_) {}
          }
        });
        let imported = 0;
        let overwritten = 0;
        const newRegistry = new Set(getDraftRegistry());
        Object.keys(items).forEach((key) => {
          const rawPath = String(key || '').slice('xdex_draft:'.length);
          // ★ 远端键规范化:旧槽(/t/{tid}/page/N 等)并入串级单槽;非串内页(/f/ 等)跳过
          const canonicalPath = normalizeDraftKeyFromPath(rawPath) || (rawPath.indexOf('/t/') === 0 ? rawPath : '');
          if (!canonicalPath) return;
          const ck = getDraftStorageKey(canonicalPath);
          // 远端已删除(账本有) → 压制;
          // 远端无删除账本但本地残留删除账本 → 本地账本失效放行(远端有内容=翻案)
          if (remoteDeletions[ck] || remoteDeletions[canonicalDraftKey(key)]) return;
          const clean = normalizeDraftExportText(items[key]);
          if (!clean) return;
          const existing = readDraftValue(canonicalPath);
          if (existing === items[key]) return;
          if (existing) overwritten++;
          else imported++;
          GM_setValue(ck, items[key]);
          newRegistry.add(ck);
          removeDraftDeletion(ck);   // 远端翻案内容落地时撤销本地残留账本
        });
        saveDraftRegistry(Array.from(newRegistry));
        return { imported, overwritten };
      }
      function mergeKaomojiStats(localStats, importedStats) {
        const local = (localStats && typeof localStats === 'object') ? localStats : {};
        const imported = (importedStats && typeof importedStats === 'object') ? importedStats : {};
        const result = Object.assign({}, local);
        Object.keys(imported).forEach((key) => {
          const impVal = imported[key];
          const localVal = result[key];
          if (!localVal) {
            result[key] = impVal;
          } else if (typeof impVal === 'object' && typeof localVal === 'object') {
            const localCount = Number(localVal.count) || 0;
            const impCount = Number(impVal.count) || 0;
            const localLast = Number(localVal.lastUsed) || 0;
            const impLast = Number(impVal.lastUsed) || 0;
            result[key] = {
              count: localCount + impCount,
              lastUsed: Math.max(localLast, impLast)
            };
          }
        });
        return result;
      }
      function collectFullExportPayload(selection) {
        const payload = {};
        if (selection.settings) {
          payload.myScriptSettings = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
        }
        if (selection.threadHistory) {
          payload.threadHistory = normalizeThreadHistoryStore(GM_getValue(THREAD_HISTORY_STORAGE_KEY, null));
        }
        if (selection.postHistory) {
          payload.postHistory = normalizePostHistoryStore(GM_getValue(POST_HISTORY_STORAGE_KEY, null));
        }
        if (selection.drafts) {
          payload.drafts = collectDraftsForExport();
        }
        if (selection.kaomojiStats) {
          payload.kaomojiStats = GM_getValue('kaomojiUsageStats', {});
        }
        if (selection.cookiePrefs) {
          payload.cookiePrefs = GM_getValue('xdex_thread_cookie_prefs', {});
        }
        if (selection.webdav) {
          try { payload.webdavConfig = GM_getValue(WEBDAV_CONFIG_KEY, null); } catch (e) { payload.webdavConfig = null; }
        }
        return payload;
      }
      function buildFullExportFile(selection) {
        const payload = collectFullExportPayload(selection);
        const now = new Date();
        const timestamp = now.toISOString().replace(/[:]/g, '-').replace(/\..+/, '');
        const scriptVersion = typeof VERSION !== 'undefined' ? VERSION : (typeof GM_info !== 'undefined' ? GM_info.script.version : 'unknown');
        const platform = typeof GM_info !== 'undefined' && GM_info.scriptHandler ? 'userscript' : 'extension';
        return {
          file: {
            meta: { format: 'xdex-full-export', schemaVersion: FULL_EXPORT_SCHEMA_VERSION, exportedAt: now.toISOString(), source: 'nmbxd-EX', scriptVersion, platform },
            selection,
            strategyHints: { settings: 'merge', threadHistory: 'merge', postHistory: 'merge', drafts: 'override-imported', kaomojiStats: 'accumulate', cookiePrefs: 'merge', webdav: 'override' },
            summary: {
              threadHistoryCount: selection.threadHistory ? Object.keys((payload.threadHistory || {}).items || {}).length : 0,
              postHistoryCount: selection.postHistory ? Object.keys((payload.postHistory || {}).items || {}).length : 0,
              draftCount: selection.drafts ? (payload.drafts.registry || []).length : 0,
              kaomojiStatsEntries: selection.kaomojiStats ? Object.keys(payload.kaomojiStats || {}).length : 0,
              cookiePrefsCount: selection.cookiePrefs ? Object.keys(payload.cookiePrefs || {}).length : 0,
              webdavConfig: selection.webdav && payload.webdavConfig ? 1 : 0,
            },
            payload
          },
          filename: `x岛-ex-full-export-${timestamp}.json`
        };
      }
      function downloadFullExportFile(fileData) {
        const blob = new Blob([JSON.stringify(fileData.file, null, 2)], { type: 'application/json' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = fileData.filename;
        a.click();
        URL.revokeObjectURL(a.href);
      }
      function parseFullExportFile(text) {
        let parsed;
        try { parsed = JSON.parse(text); } catch (e) { return { valid: false, error: '文件格式错误,无法解析 JSON' }; }
        if (!parsed || typeof parsed !== 'object') return { valid: false, error: '文件内容无效' };
        const meta = parsed.meta || {};
        if (meta.format !== 'xdex-full-export') return { valid: false, error: '不支持的文件格式,请使用 X岛-EX 使用数据导出文件' };
        if (!meta.schemaVersion || meta.schemaVersion > FULL_EXPORT_SCHEMA_VERSION) return { valid: false, error: `不支持的 schema 版本: ${meta.schemaVersion}` };
        if (!parsed.payload || typeof parsed.payload !== 'object') return { valid: false, error: '文件缺少 payload 数据' };
        return { valid: true, data: parsed };
      }
      function applyFullImportPayload(importData, options) {
        options = options || {};
        const payload = importData.payload;
        const report = {};
        if (payload.myScriptSettings) {
          const local = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
          const merged = mergeSettingsForFullImport(local, payload.myScriptSettings);
          GM_setValue(SettingPanel.key, merged);
          report.settings = { mode: 'merge', changed: true };
        }
        if (payload.threadHistory) {
          const local = normalizeThreadHistoryStore(GM_getValue(THREAD_HISTORY_STORAGE_KEY, null));
          let merged;
          let mode = 'merge';
          if (options.threadHistoryMode === 'webdav-delta') {
            // WebDAV:基于基线计算独立贡献,不直接累加
            let baselines = getWebdavHistoryBaselines();
            // 迁移兜底:基线从未建立(功能首次引入/基线丢失)但本端此前同步过时,本地存储已是
            // 历史合并结果;若按 baseline=0 计算,整份旧计数会被当成新增,导致各端逐轮倍增
            if (!baselines.threadHistory) {
              let syncedBefore = 0;
              try { syncedBefore = Number((GM_getValue(WEBDAV_CONFIG_KEY, null) || {}).lastSyncAt) || 0; } catch (e) {}
              if (syncedBefore > 0) {
                baselines = saveWebdavHistoryBaselinesFromStore(local, baselines);
                console.log('[webdav] 基线缺失且本端曾同步过:已按当前本地值重建基线,避免旧计数被计为新增');
              }
            }
            merged = mergeThreadHistoryStoreWebdav(local, normalizeThreadHistoryStore(payload.threadHistory), baselines);
            merged = setThreadHistoryStore(merged);
            saveWebdavHistoryBaselinesFromStore(merged, baselines);
            mode = 'webdav-delta';
          } else {
            // 本地导入导出:浏览次数累加
            merged = mergeThreadHistoryStore(local, payload.threadHistory);
            merged = setThreadHistoryStore(merged);
          }
          report.threadHistory = { mode, count: Object.keys(merged.items || {}).length };
        }
        if (payload.postHistory) {
          const local = normalizePostHistoryStore(GM_getValue(POST_HISTORY_STORAGE_KEY, null));
          const merged = setPostHistoryStore(mergePostHistoryStore(local, payload.postHistory));
          // 经 setter 写入:同步刷新归一化缓存并广播变更
          report.postHistory = { mode: 'merge', count: Object.keys(merged.items || {}).length };
        }
        // 草稿暂为纯本地数据: WebDAV 下载侧跳过导入, 远端旧草稿不再回流
        if (payload.drafts && options.threadHistoryMode !== 'webdav-delta') {
          const result = applyDraftsFromImport(payload.drafts);
          report.drafts = { mode: 'override-imported', imported: result.imported, overwritten: result.overwritten };
        }
        if (payload.kaomojiStats) {
          const local = GM_getValue('kaomojiUsageStats', {});
          let merged;
          if (options.threadHistoryMode === 'webdav-delta') {
            // WebDAV 双向同步:按各端基线计算独立贡献后加算,避免每轮把远端全量当新增而重复计数;
            // lastUsed 恒取两端较大值(时间不可累加)
            const baselines = getWebdavHistoryBaselines();
            const kaoBase = (baselines && baselines.kaomojiStats) || {};
            merged = {};
            const keys = new Set(Object.keys(local).concat(Object.keys(payload.kaomojiStats)));
            keys.forEach((key) => {
              const l = local[key];
              const r = payload.kaomojiStats[key];
              if (!r) { merged[key] = l; return; }
              if (!l || typeof l !== 'object' || typeof r !== 'object') { merged[key] = r; return; }
              const baseCount = Number((kaoBase[key] && kaoBase[key].count) || 0);
              const delta = Math.max(0, (Number(l.count) || 0) - baseCount);
              merged[key] = {
                count: (Number(r.count) || 0) + delta,
                lastUsed: Math.max(Number(l.lastUsed) || 0, Number(r.lastUsed) || 0)
              };
            });
            // 推进基线到合并值(与浏览历史同模式:回推失败时由外层快照统一回滚)
            const newBaselines = Object.assign({}, baselines || {});
            newBaselines.kaomojiStats = {};
            Object.keys(merged).forEach((key) => {
              const entry = merged[key];
              newBaselines.kaomojiStats[key] = { count: (entry && Number(entry.count)) || 0, at: Date.now() };
            });
            try { GM_setValue('xdex_webdav_history_baselines', newBaselines); } catch (e) {}
          } else {
            // 手动导入导出:保持原有纯累加语义
            merged = mergeKaomojiStats(local, payload.kaomojiStats);
          }
          GM_setValue('kaomojiUsageStats', merged);
          report.kaomojiStats = { mode: 'accumulate', changed: true };
        }

        if (payload.cookiePrefs) {
          const local = GM_getValue('xdex_thread_cookie_prefs', {});
          const merged = Object.assign({}, local, payload.cookiePrefs);
          GM_setValue('xdex_thread_cookie_prefs', merged);
          report.cookiePrefs = { mode: 'merge', count: Object.keys(merged).length };
        }
        if (payload.webdavConfig && typeof payload.webdavConfig === 'object') {
          try { GM_setValue(WEBDAV_CONFIG_KEY, payload.webdavConfig); } catch (e) {}
          report.webdavConfig = { mode: 'override', changed: true };
        }

        return report;
      }
      // 暴露给 tag29 WebDAV 使用:本组工具位于 render 嵌套作用域,顶层不可见
      window.__xdexWebdavUtils = {
        buildFullExportFile,
        parseFullExportFile,
        applyFullImportPayload,
        mergeThreadHistoryStoreWebdav,
        mergePostHistoryStore,
        mergeSettingsForWebdavUpload,
        getWebdavHistoryBaselines,
        saveWebdavHistoryBaselinesFromStore
      };
      // === 使用数据导入导出 end ===
      function buildJSONC(state) {
        const filtered = {};
        for (const [k, v] of Object.entries(state)) {
          if (!FIXED_KEYS.has(k)) filtered[k] = v; // 不清理零宽字符,保留用户原始数据
        }
        const meta = {
          _meta: {
            version: (typeof VERSION !== 'undefined' ? VERSION : GM_info.script.version),
            exportedAt: new Date().toISOString(),
            source: 'nmbxd-EX'
          },
          settings: filtered,
          webdavConfig: (function () {
            try { return GM_getValue(WEBDAV_CONFIG_KEY, null); } catch (e) { return null; }
          })()
        };
        const lines = JSON.stringify(meta, null, 2).split('\n');
        // 在 _meta 前加注释,在 settings 闭合括号前加尾随逗号
        let result = lines.map((line, i) => {
          if (i === 0) return line; // 开头 {
          if (/"_meta"/.test(line)) return '  // X岛-EX 配置文件\n' + line;
          if (/^}$/.test(line.trim())) return line; // 最外层 }
          return line;
        }).join('\n');
        // 给 settings 对象的最后一个字段后加尾随逗号
        result = result.replace(/("settings":\s*\{[\s\S]*?)(\n\s*\}\s*\n\s*\})/, (m, inner, close) => {
          const trimmed = inner.replace(/,\s*$/, '');
          return trimmed + ',\n' + close.replace(/^\n/, '');
        });
        return result;
      }
      function validateImport(incoming) {
        const defaults = SettingPanel.defaults;
        if (typeof incoming !== 'object' || incoming === null) {
          toast('配置内容无效'); return null;
        }
        const validated = {};
        let skipped = 0;
        for (const [key, val] of Object.entries(incoming)) {
          if (FIXED_KEYS.has(key)) continue;         // 固定项直接跳过
          if (!(key in defaults)) continue;           // 未知字段跳过
          if (Array.isArray(defaults[key]) && !Array.isArray(val)) { skipped++; continue; }
          if (typeof val !== typeof defaults[key]) { skipped++; continue; }
          validated[key] = val;
        }
        return Object.assign({}, defaults, validated);
      }
      function handleImportText(text) {

        if (!text || !text.trim()) { toast('内容为空'); return; }
        // 只清理文本开头的 BOM,不清理内容中的零宽字符(用户数据可能包含零宽空格)
        text = text.replace(/^\uFEFF/, '');
        let parsed;
        try {
          parsed = parseJSONC(text);
        } catch (e) {
          toast('配置文件格式错误'); return;
        }
        const incoming = parsed.settings || parsed;
        const merged = validateImport(incoming);
        if (!merged) return;
        // WebDAV 配置随设置文件一并导入(独立 GM key,不进 settings 白名单)
        if (parsed && parsed.webdavConfig && typeof parsed.webdavConfig === 'object') {
          try { GM_setValue(WEBDAV_CONFIG_KEY, parsed.webdavConfig); } catch (e) {}
        }
        SettingPanel.__pendingImport = merged;
        // 显示保存按钮
        $('#btn_sp_importExport').removeClass('xdex-btn-hidden');
        toast('格式正确,请点击[应用]');
      }
      // 从剪贴板导入
      $('#sp_importClipboard').off('click').on('click', async () => {
        try {
          const text = await navigator.clipboard.readText();
          handleImportText(text);
        } catch (e) {
          toast('无法读取剪贴板,请先点击页面后再试');
        }
      });
      // 导出到剪贴板
      $('#sp_exportClipboard').off('click').on('click', async () => {
        try {
          const jsonc = buildJSONC(SettingPanel.state);
          await navigator.clipboard.writeText(jsonc);
          toast('已复制到剪贴板');
        } catch (e) {
          toast('复制失败');
        }
      });
      // 从文件导入
      $('#sp_importFile').off('click').on('click', () => {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json,.jsonc,.txt';
        input.onchange = () => {
          const file = input.files && input.files[0];
          if (!file) return;
          const reader = new FileReader();
          reader.onload = () => handleImportText(reader.result);
          reader.onerror = () => toast('文件读取失败');
          reader.readAsText(file);
        };
        input.click();
      });
      // 导出为文件
      $('#sp_exportFile').off('click').on('click', () => {
        try {
          const jsonc = buildJSONC(SettingPanel.state);
          const blob = new Blob([jsonc], { type: 'application/json' });
          const a = document.createElement('a');
          a.href = URL.createObjectURL(blob);
          a.download = 'x岛-ex-settings-v' + (typeof VERSION !== 'undefined' ? VERSION : GM_info.script.version) + '.jsonc';
          a.click();
          URL.revokeObjectURL(a.href);
          toast('配置已导出');
        } catch (e) {
          toast('导出失败');
        }
      });
      // 导入/导出折叠块的"保存"按钮
      $('#btn_sp_importExport').off('click').on('click', e => {
        e.stopPropagation();
        if (SettingPanel.__pendingImport) {
          GM_setValue(SettingPanel.key, SettingPanel.__pendingImport);
          SettingPanel.state = SettingPanel.__pendingImport;
          delete SettingPanel.__pendingImport;
          $('#btn_sp_importExport').addClass('xdex-btn-hidden');
          toast('配置已导入,即将刷新');
          setTimeout(() => location.reload(), 800);
        } else {
          toast('没有待导入的配置');
        }
      });
      // 关闭面板时清除暂存
      const _origClose = $('#sp_close,#sp_cover').off.bind($('#sp_close,#sp_cover'), 'click');
      delete SettingPanel.__pendingImport;
      // ── 使用数据导入导出 ──
      $('#btn_sp_fullExport_reset').off('click').on('click', (e) => {
        e.stopPropagation();
        const selection = {
          settings: $('#sp_fullExport_settings').is(':checked'),
          threadHistory: $('#sp_fullExport_threadHistory').is(':checked'),
          postHistory: $('#sp_fullExport_postHistory').is(':checked'),
          drafts: $('#sp_fullExport_drafts').is(':checked'),
          kaomojiStats: $('#sp_fullExport_kaomojiStats').is(':checked'),
          cookiePrefs: $('#sp_fullExport_cookiePrefs').is(':checked'),
          webdav: $('#sp_fullExport_webdav').is(':checked')
        };
        if (!Object.values(selection).some(Boolean)) { toast('请至少勾选一项'); return; }
        const parts = [];
        if (selection.settings) parts.push('设置(恢复默认)');
        if (selection.threadHistory) parts.push('浏览历史');
        if (selection.postHistory) parts.push('发言历史');
        if (selection.drafts) parts.push('草稿');
        if (selection.kaomojiStats) parts.push('颜文字统计');
        if (selection.cookiePrefs) parts.push('串内饼干偏好');
        if (selection.webdav) parts.push('WebDAV 配置');
        if (!window.confirm(`确定要清除以下项目的全部内容吗?\n\n${parts.join('、')}\n\n清除后页面将自动刷新。`)) return;
        try {
          if (selection.settings) GM_setValue(SettingPanel.key, {});
          if (selection.threadHistory) setThreadHistoryStore(null);
          if (selection.postHistory) setPostHistoryStore(null);
          if (selection.drafts) {
            getDraftRegistry().forEach((key) => { try { GM_deleteValue(key); } catch (_) {} });
            saveDraftRegistry([]);
          }
          if (selection.kaomojiStats) GM_setValue('kaomojiUsageStats', {});
          if (selection.cookiePrefs) GM_setValue('xdex_thread_cookie_prefs', {});
          if (selection.webdav) GM_setValue(WEBDAV_CONFIG_KEY, {});
          toast('已清除所选项目,即将刷新');
          setTimeout(() => location.reload(), 800);
        } catch (err) {
          toast('清除失败: ' + (err.message || err));
        }
      });
      $('#btn_sp_fullExport_export').off('click').on('click', (e) => {
        e.stopPropagation();
        const selection = {
          settings: $('#sp_fullExport_settings').is(':checked'),
          threadHistory: $('#sp_fullExport_threadHistory').is(':checked'),
          postHistory: $('#sp_fullExport_postHistory').is(':checked'),
          drafts: $('#sp_fullExport_drafts').is(':checked'),
          kaomojiStats: $('#sp_fullExport_kaomojiStats').is(':checked'),
          cookiePrefs: $('#sp_fullExport_cookiePrefs').is(':checked'),
          webdav: $('#sp_fullExport_webdav').is(':checked')
        };
        if (!Object.values(selection).some(Boolean)) { toast('请至少勾选一项'); return; }
        const fileData = buildFullExportFile(selection);
        downloadFullExportFile(fileData);
        toast('使用数据已导出');
      });
      $('#btn_sp_fullExport_import').off('click').on('click', (e) => {
        e.stopPropagation();
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.onchange = () => {
          const file = input.files && input.files[0];
          if (!file) return;
          const reader = new FileReader();
          reader.onload = () => {
            const result = parseFullExportFile(reader.result);
            if (!result.valid) { toast(result.error); return; }
            SettingPanel.__pendingFullImport = result.data;
            renderFullImportPreview(result.data);
          };
          reader.readAsText(file);
        };
        input.click();
      });
      function renderFullImportPreview(data) {
        const $preview = $('#sp_fullExport_import_preview').empty().show();
        const meta = data.meta || {};
        const summary = data.summary || {};
        let html = '<div style="font-size:12px;color:var(--foreground, #333);">';
        html += `<div>来源版本: ${meta.scriptVersion || '?'} | 导出时间: ${meta.exportedAt ? new Date(meta.exportedAt).toLocaleString() : '?'}</div>`;
        html += '<div style="margin-top:4px;">包含数据:</div><ul style="margin:2px 0;padding-left:20px;">';
        if (summary.threadHistoryCount) html += `<li>浏览历史: ${summary.threadHistoryCount} 条</li>`;
        if (summary.postHistoryCount) html += `<li>发言历史: ${summary.postHistoryCount} 条</li>`;
        if (summary.draftCount) html += `<li>草稿: ${summary.draftCount} 条</li>`;
        if (summary.kaomojiStatsEntries) html += `<li>颜文字统计: ${summary.kaomojiStatsEntries} 项</li>`;
        if (summary.cookiePrefsCount) html += `<li>串内饼干偏好: ${summary.cookiePrefsCount} 条</li>`;
        if (summary.webdavConfig) html += '<li>WebDAV 配置(覆盖导入)</li>';
        if (data.selection && data.selection.settings) html += '<li>设置配置(合并导入)</li>';
        html += '</ul>';
        html += '<div style="color:var(--muted-foreground, #666);margin-top:4px;">导入策略: 设置合并、历史合并、草稿冲突时导入端覆盖、颜文字累加</div>';
        html += '</div>';
        const $btn = $('<button style="margin-top:6px;padding:4px 10px;">应用导入</button>');
        $btn.on('click', () => {
          if (!SettingPanel.__pendingFullImport) { toast('无可导入数据'); return; }
          if (!window.confirm('确定要导入使用数据吗?\n导入完成后页面将自动刷新。')) return;
          const report = applyFullImportPayload(SettingPanel.__pendingFullImport);
          SettingPanel.__pendingFullImport = null;
          $preview.hide();
          const parts = [];
          if (report.settings) parts.push('设置已合并');
          if (report.threadHistory) parts.push(`浏览历史 ${report.threadHistory.count} 条`);
          if (report.postHistory) parts.push(`发言历史 ${report.postHistory.count} 条`);
          if (report.drafts) parts.push(`草稿 ${report.drafts.imported} 条导入${report.drafts.overwritten ? `, ${report.drafts.overwritten} 条覆盖` : ''}`);
          if (report.kaomojiStats) parts.push('颜文字已累加');
          toast(`使用数据导入完成: ${parts.join('、')},即将刷新`);
          setTimeout(() => location.reload(), 800);
        });
        $preview.html(html).append($btn);
      }
      $('#sp_apply').off('click').on('click', ()=>{
        collectReloadRequiredSettingsFromPanel();
        let valid = true;
        const blockedKeywords = collectBlockedKeywordGroupsFromPanel();
        if (!blockedKeywords) return;
        this.state.blockedKeywords = blockedKeywords;
        const favoriteThreads = spUi('collectFavoriteThreadsFromPanel');
        if (!favoriteThreads) return;
        this.state.favoriteThreads = favoriteThreads;
        this.state.replyModeDefault = $('#sp_replyModeDefault').val();
        this.state.replyExtraDefault = $('#sp_replyExtraDefault').val();
        this.state.kaomojiSort = $('#sp_kaomojiSort').val() || 'default';
        this.state.dockDisplayMode = $('#sp_dockDisplayMode').val() || 'hover';
        this.state.applyImageHideMode = $('#sp_applyImageHideMode').val() || 'default';
        this.state.imageViewerSeparators = $('#sp_imageViewerSeparators').is(':checked');
        this.state.threadCookieWhitelistDisplayMode = $('#sp_threadCookieWhitelistDisplayMode').val() || 'fold';
        this.state.poAnnotationSideDisplayMode = $('#sp_poAnnotationSideDisplayMode').val() || 'collapse';
        this.state.timeDisplayMode = ($('#sp_timeDisplayMode').val() === 'exact') ? 'exact' : 'relative';
        // 标记分组(双字段结构)
        const mk = collectMarkedGroupsFromPanel();
        if (!mk) return;
        this.state.markedGroups = mk;
        // 屏蔽分组(双字段结构)
        const bk = [];
        $('#blocked-inputs-container .blocked-row').each((idx, el)=>{
          const $row = $(el);
          const desc = ($row.find('.blocked-desc-input').val() || '').trim();
          const cookies = Utils.strToList(($row.find('.blocked-cookies-input').val() || '').trim());
          if (!desc && !cookies.length) return;
          if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid=false; return false; }
          if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid=false; return false; }
          if (cookies.some(id=>!Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid=false; return false; }
          bk.push({ desc, cookies });
        });
        if (!valid) return;
        this.state.blockedCookies = bk;
        const wlg = [];
        $('#thread-cookie-whitelist-inputs-container .thread-cookie-whitelist-row').each((idx, el) => {
          const $row = $(el);
          const desc = ($row.find('.thread-cookie-whitelist-desc-input').val() || '').trim();
          const threads = Utils.strToList(($row.find('.thread-cookie-whitelist-threads-input').val() || '').trim());
          const cookies = Utils.strToList(($row.find('.thread-cookie-whitelist-cookies-input').val() || '').trim());
          if (!desc && !threads.length && !cookies.length) return;
          if (!spData('isValidDesc', desc)) { toast(`第${idx + 1}条备注过长`); valid = false; return false; }
          if (!threads.length) { toast(`第${idx + 1}条未指定串号`); valid = false; return false; }
          if (threads.some(id => !spData('isValidThreadId', id))) { toast(`第${idx + 1}条存在不合法串号`); valid = false; return false; }
          if (!cookies.length) { toast(`第${idx + 1}条未指定饼干`); valid = false; return false; }
          if (cookies.some(id => !Utils.cookieLegal(id))) { toast(`第${idx + 1}条存在不合法饼干`); valid = false; return false; }
          wlg.push({ desc, threads, cookies, rowIndex: idx + 1 });
        });
        if (!valid) return;
        const { groups: mergedWhitelistGroups, mergeEvents } = spData('mergeThreadCookieWhitelistGroups', wlg);
        this.state.threadCookieWhitelistGroups = mergedWhitelistGroups;
        mergeEvents.forEach((event) => {
          if (event.desc) {
            toast(`第${event.rowIndex}条(${event.desc})中的串号 ${event.threadId} 已与现有只看规则合并`);
          } else {
            toast(`第${event.rowIndex}条的串号 ${event.threadId}(${event.cookies.join(',')})已与现有只看规则合并`);
          }
        });
        // 原版
        // GM_setValue(this.key, this.state);
        // toast('保存成功,即将刷新页面');
        // setTimeout(()=>location.reload(),500);
        // Edge双重刷新版
        // GM_setValue(this.key, this.state);
        // console.log('GM_setValue执行成功');
        // // 立即读取验证
        // const saved = GM_getValue(this.key);
        // console.log('读取验证:', JSON.stringify(saved));
        // toast('保存成功,即将刷新页面');
        // // Edge 浏览器需要更长的延迟确保存储完成
        // setTimeout(() => {
        //   // 再次验证保存是否成功
        //   const finalCheck = GM_getValue(this.key);
        //   console.log('刷新前最终验证:', JSON.stringify(finalCheck));
        //   // 两次刷新
        //   try {
        //     // 设置标记,下一次页面加载时脚本会检测到并执行第二次重载
        //     localStorage.setItem(this.key + '_doSecondReload', '1');
        //   } catch (e) {
        //     console.warn('[Settings] set second-reload flag failed', e);
        //   }
        //   location.reload();
        // }, 800);  // 将延迟从 500ms 增加到 800ms
        // 当前版本
        GM_setValue(this.key, this.state);
        console.log('GM_setValue执行成功');
        // 立即读取验证
        const saved = GM_getValue(this.key);
        console.log('读取验证:', JSON.stringify(saved));
        toast('保存成功,即将刷新页面');
        // Edge 浏览器需要更长的延迟确保存储完成
        setTimeout(() => {
          // 再次验证保存是否成功
          const finalCheck = GM_getValue(this.key);
          console.log('刷新前最终验证:', JSON.stringify(finalCheck));
          // 只刷新一次
          location.reload();
        }, 800);  // 将延迟从 500ms 增加到 800ms
      });
      // 关闭面板
      $('#sp_close,#sp_cover').off('click').on('click', e=>{
        if (e.target.id==='sp_close' || e.target.id==='sp_cover')
          $('#sp_cover').fadeOut();
      });
      //鼠标悬浮在具体功能上显示提示
      // ====== 1. 定义功能描述映射表 ======
      const spDescriptions = {
        sp_enableCookieSwitch: '发帖框上方添加饼干切换器,单击即可快速切换饼干。使用前可单击“刷新”以获取当前登陆账户最新饼干列表',
        sp_enableCookieConfirm: '发送前弹窗显示当前串内各饼干的使用情况,可切换饼干后再发送',
        sp_enablePaginationDuplication: '在串首页添加页码导航栏,并拓展为最长七个页码按钮',
        sp_disableWatermark: '取消发图默认勾选的水印选项',
        sp_updatePreviewCookie: '为“增强X岛匿名版”添加的预览框显示真实饼干',
        sp_hideEmptyTitleEmail: '隐藏帖内无标题、无名氏和版规提示,优化显示效果,减少版面占用',
        sp_enableExternalImagePreview: '直接显示外部图床的图片',
        sp_enableUpdateCheck: '控制是否自动检查脚本更新;关闭后不会发起远程更新请求,也不会继续安排后续检查',
        sp_enableAutoCookieRefresh: '回到X岛页面后自动刷新饼干,以防错饼',
        sp_enableAutoCookieRefreshToast: '自动刷新时显示toast提示,触发频率较高,建议关闭',
        sp_enableSeamlessPaging: '阅读到页面底部时无缝加载下一页并为新页首添加页码提示',
        sp_enableAutoSeamlessPaging: '滚动到页面底部后自动触发无缝翻页,关闭则可使用按钮手动无缝翻页',
        sp_enableHDImageAndLayoutFix: 'X岛-揭示板的增强型体验:默认加载原图而非缩略图,并为所有图片添加X岛自带图片控件;调整布局,防止文字与图片溢出',
        sp_enableImageContextMenu: 'userscript模式:为图片/动图启用自定义右键菜单,关闭后保留浏览器原生图片右键菜单,复制图片过程中需要浏览器窗口在前台。\nextension模式:在浏览器右键菜单中添加“X岛-EX:复制GIF/APNG”按钮,仅用于复制GIF/APNG,在复制GIF/APNG过程中焦点可不在前台',
        sp_enableLinkBlank: 'X岛-揭示板的增强型体验:串页链接在新标签页打开',
        sp_enableAutoUrlLinkify: '自动将正文中的网址转换为可点击的新标签页蓝色链接,可与“拓展引用格式”共存',
        sp_enableQuotePreview: '优化引用弹窗显示,将鼠标悬停出现引用弹窗改为点击显示引用弹窗,引用弹窗可持久存在,支持嵌套、拖拽,点击非引用弹窗区域或ESC键可关闭当前引用弹窗,点击右下角×以关闭全部引用弹窗',
        sp_extendQuote: '拓展引用格式,支持除“>>No.66994128”标准引用格式外的引用,例如“>>66994128”、“66994128”、“No.66994128”,同样支持“优化引用弹窗”',
        sp_extendQuoteAvailabilityDetection: '检测引用号对应的串或回复是否存在以及属于什么类型:恢复-默认;不存在-变淡;主串-实线(当前串)/虚线(其他串)。被标记为主串的引用号可从“拓展引用浮窗”中直接跳转',
        sp_enableCustomDarkTheme: '深色模式(light=浅色 / dark=深色)',
        sp_threadCookieWhitelistModeEnabled: '只看饼干模式\n折叠:保持原版只看饼干折叠逻辑\n隐藏:未命中的回复直接隐藏\n分栏:重点回复保留在主阅读流,观众回复进入侧栏批注\n可选观众回复的展开/收起',
        sp_poAnnotationSideDisplayMode: '分栏模式下观众回复栏的显示状态。展开:完整展开;收起:默认高度不超过对应主回复高度,超出部分滚动',
        sp_toggleSidebar: '来自acVMxuv的自动收起右侧扩展坞侧边栏,鼠标悬停时展开显示',
        sp_updateReplyNumbers: '添加当页内回复编号显示',
        sp_replaceRightSidebar: '增强右侧扩展坞功能,点击REPLY按钮打开回复弹窗,点击非回复弹窗区域或ESC键可关闭回复弹窗,另外支持使用CTRL+ENTER发送消息',
        sp_interceptReplyForm: '拦截回复跳转中间页,使用toast提示发送成功/失败信息',
        sp_interceptReplyFormUnvcode: '不可明说的功能,请参照https://words-away.typeboom.com/说明',
        sp_interceptReplyFormU200B: '优先使用插入零宽空格模式而非unvcode替换模式',
        sp_interceptReplyFormAutoCompress: '自动压缩>2048KB的图片',
        sp_kaomojiEnhancer: '拓展颜文字功能,添加更多颜文字(部分来自蓝岛),优化选择颜文字弹窗,选择颜文字后可插入光标所在处。支持排序:默认(原顺序)/常用(使用次数高优先)/最近(最近使用优先,未使用保持默认顺序)',
        sp_highlightPO: '为回复添加Po主标志,PO主回复编号使用角标显示',
        sp_enhancePostFormLayout: '优化发串/回复表单布局,将“送出”按钮移至颜文字栏目,折叠“标题”“E-mail”“名称”等不常用项目,节省版面,添加标题/作者/E-mail/正文的字数统计与提醒',
        sp_applyFilters: '标记/屏蔽-饼干/关键词过滤规则\n折叠:匹配到的串/回复显示为可展开的按钮\n隐藏:匹配到的串/回复完全隐藏',
        sp_enhanceIsland: '增强X岛匿名版:\n1.发串前显示预览:麻麻再也不用担心我的ASCII ART排版失误了,另外支持预览插入图片和外部图床图片;\n2.自动保存编辑:记忆文本框内容(防止屏蔽词导致被吞),可以在翻页等各种页面切换后保存,仅在“回复成功”后删除,按主串号 "/t/xxxx" 分开存储;\n3.追记引用串号:点击串号回复时附加到光标所在处(或替换文本选区),可追记多条引用;\n4.人类友好的时间显示:如“5秒前”、“1小时前”、“昨天”等;\n5.粘贴插入图片:直接粘贴,将自动作为图片插入\n自动添加标题:将po主设置的标题或者第一行文字 + 页码设置为标签页标题',
        sp_timeDisplayMode: '切换串内时间显示方式。相对时间会在当前可见页面定时刷新;精确时间显示原始发帖时间',
        sp_replyQuicklyOnBoardPage: '为板块页添加快速回复模式,在板块页即可回串,页面实时更新,无需跳转串内;并额外支持时间线内回串。\n“板块页默认模式”可选“发串/回复”两种模式,“回复默认模式”可选“临时/连续”两种回复模式,临时模式下回复成功即清除回串信息,连续模式可连续回复直到手动清理回串信息,搭配回复浮窗使用效果更佳',
        sp_enablePostExpand: '为板块页内串添加“展开/收起”按钮,点击即可切换长串的完整显示与折叠显示',
        sp_searchServiceBy4sY: '官方搜索当前不可用,公告详见:https://www.nmbxd1.com/t/56546294\n替换搜索按钮为来自4sYbzEX的“野生搜索酱”,具体使用方法请查阅原串:https://www.nmbxd.com/t/64792841',
        sp_enableImageHideMode: '“默认/模糊/无图/Tips”四种模式可选。默认模式不做修改;选择模糊模式时可使用鼠标悬浮暂时预览图片;无图模式隐藏图片;Tips模式随机显示Tips娘,点击后可恢复原图显示',
        sp_enableFavoriteThreads: '在侧边栏添加常用串,支持串内一键添加,并优先跳转浏览历史中的最近阅读页',
        sp_enableThreadHistory: '保存浏览历史,支持搜索,可切换多种排序方式。关闭后不再记录新的浏览数据(已有数据保留),侧边栏入口隐藏',
        sp_enablePostHistory: '保存发言历史,分为“我的主题/我的回复”,并记录回复所在页面,支持搜索,可切换多种排序方式。关闭后不再在发送消息后自动记录(手动添加不受影响,已有数据保留),侧边栏入口隐藏',
        sp_enableSubscriptionFeed: '使用移动端订阅号进行同步,支持添加多个订阅号',
        sp_enableThreadExport: '导出串:把当前串(或指定串号)导出为 Markdown / TXT 文件。支持批量串号、页码范围与「只看 PO」;任务在后台按顺序执行,页面可关闭。右侧图标按钮可直接打开导出窗口',
        sp_enableImageViewerMode: '阅图模式:以瀑布流方式浏览当前串的所有图片,点击单图进入详情,支持旋转、缩放、键盘翻页(←→方向键切换、[]旋转、+-缩放、0复位、↑↓平移)。右侧图标按钮可在设置面板内进入;无图模式下右上角入口会隐藏,可用此按钮作为备选',
        sp_imageViewerSeparators: '在各页图片之间显示红色分隔线与“第N页”页码标识;关闭以获得更纯净的阅图体验',
        sp_postAfterAction: '发串成功后的行为:新标签页打开新串,或刷新当前板块页回到顶部',
        sp_subscriptionFeeds: '管理X岛订阅号,可添加多个订阅号并设置备注,用于在"我的订阅"标签中查看和管理订阅内容',
        sp_disableAutoQuote: '在类似https://www.nmbxd1.com/t/67024789?page=23&r=68811442等携带r=参数的串中,保留高亮的同时控制其是否在输入框中自动添加引用号,值班室版块默认不生效',
        sp_autoSelectReportReason: '值班室默认选择“其他”理由,且在值班室版块快捷回复面板中默认为“发串”模式以便提交举报',
      };
      // 更新日志弹窗(放在 spDescriptions 之后,避免引用未定义)
      if (!document.getElementById('sp_update_log')) {
        const $log = $(
          '<div id=\"sp_update_log\" style=\"display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:10001;\">' +
            '<div style=\"position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(640px, calc(100vw - 48px));max-height:calc(100vh - 96px);display:flex;flex-direction:column;overflow:hidden;background:var(--xdex-sp-panel-bg);border:1px solid var(--xdex-sp-border);border-radius:8px;box-shadow:0 2px 12px var(--xdex-sp-shadow);\">' +
              '<div style=\"padding:10px 12px;border-bottom:1px solid var(--xdex-sp-border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;\">' +
                '<span class=\"xdex-update-log-title\" style=\"font-weight:bold;\">更新日志</span>' +
                '<span id=\"sp_update_log_close\" style=\"cursor:pointer;\">✕</span>' +
              '</div>' +
              '<div class=\"xdex-update-log-body\" style=\"padding:12px;font-size:12px;white-space:pre-line;overflow:auto;flex:1 1 auto;min-height:0;\">' +
                (CHANGELOG || '暂无更新说明') +
              '</div>' +
              '<div class=\"xdex-update-log-actions\" style=\"display:none;padding:0 12px 12px;gap:8px;justify-content:flex-end;flex-shrink:0;\">' +
                '<button id=\"sp_update_log_update_now\" style=\"padding:4px 10px;\">立即更新</button>' +
                '<button id=\"sp_update_log_ignore_version\" style=\"padding:4px 10px;\">忽略此版本</button>' +
                '<button id=\"sp_update_log_dismiss_today\" style=\"padding:4px 10px;\">今日关闭</button>' +
              '</div>' +
            '</div>' +
          '</div>'
        );
        $('body').append($log);
        $('#sp_update_log_close,#sp_update_log').on('click', (e) => {
          if (e.target.id === 'sp_update_log') {
            spUpdate('closeUpdateLogDialog', { treatAsDismiss: false, reason: 'overlay' });
            return;
          }
          if (e.target.id === 'sp_update_log_close') {
            const isRemoteMode = $('#sp_update_log').attr('data-update-mode') === 'remote';
            spUpdate('closeUpdateLogDialog', { treatAsDismiss: isRemoteMode, reason: 'close-button' });
          }
        });
        $('#sp_update_log_update_now').on('click', () => {
          const state = spUpdate('getUpdateCheckState');
          spUpdate('closeUpdateLogDialog', { reason: 'update-now' });
          spUpdate('flashFooterUpdateHighlight', state.pendingUpdateSource || '');
        });
        $('#sp_update_log_ignore_version').on('click', () => {
          const state = spUpdate('getUpdateCheckState');
          if (state.pendingUpdateVersion) {
            state.ignoredVersion = state.pendingUpdateVersion;
            spUpdate('setUpdateCheckState', state);
            spUpdate('updateSettingsButtonBadge', state);
          }
          spUpdate('closeUpdateLogDialog', { reason: 'ignore-version' });
        });
        $('#sp_update_log_dismiss_today').on('click', () => {
          spUpdate('closeUpdateLogDialog', { treatAsDismiss: true, reason: 'dismiss-today' });
        });
      }
      // 版本号同步到当前脚本版本
      try {
        const ver = (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) ? GM_info.script.version : '';
        if (ver) $('#sp_version_link').text('v' + ver);
      } catch (e) {}
      $('#sp_version_link').off('click').on('click', (e) => {
        e.preventDefault();
        spUpdate('openUpdateLogDialog', 'local');
      });
// ====== 2. 创建 tooltip 元素并添加样式 ======
      if (!$('#sp_tooltip').length) {
        $('body').append('<div id="sp_tooltip"></div>');
        const tooltipStyle = `
          #sp_tooltip {
            position: fixed;
            max-width: 260px;
            background: rgba(0,0,0,0.85);
            color: #fff;
            padding: 6px 10px;
            border-radius: 4px;
            font-size: 12px;
            line-height: 1.4;
            pointer-events: none;
            display: none;
            z-index: 100000;
            box-shadow: 0 2px 6px rgba(0,0,0,0.3);
            transition: opacity 0.15s ease;
            opacity: 0;
            white-space: pre-line;
          }
          #sp_tooltip.show {
            display: block;
            opacity: 1;
          }
        `;
        $('<style>').text(tooltipStyle).appendTo('head');
      }
      // ====== 3. 绑定事件到每个设置项 ======
      $('#sp_checkbox_container input[type=checkbox]').each(function(){
        const id = this.id;
        const desc = spDescriptions[id];
        if (!desc) return;
        const $label = $(this).next('label');
        const $target = $label.length ? $label : $(this);
        $target.on('mouseenter', function(e){
          $('#sp_tooltip').text(desc)
            .css({ top: e.clientY + 12, left: e.clientX + 12 })
            .addClass('show');
        }).on('mousemove', function(e){
          const offsetX = 40; // 横向偏移量
          const offsetY = 0; // 纵向偏移量
          $('#sp_tooltip').css({
            top: e.clientY + offsetY,   // 保持纵向偏移
            left: e.clientX + offsetX   // 横向偏移改大,右移更多
          });
        }).on('mouseleave', function(){
          $('#sp_tooltip').removeClass('show');
        });
      });
      // 给额外的标题绑定提示
      $('#sp_replyQuicklyOnBoardPage').each(function(){
        const id = this.id;
        const desc = spDescriptions[id];
        if (!desc) return;
        $(this).on('mouseenter', function(e){
          $('#sp_tooltip').text(desc)
            .css({ top: e.clientY + 12, left: e.clientX + 12 })
            .addClass('show');
        }).on('mousemove', function(e){
          $('#sp_tooltip').css({ top: e.clientY, left: e.clientX + 40 });
        }).on('mouseleave', function(){
          $('#sp_tooltip').removeClass('show');
        });
      });
    },

    syncInputs() {
      // 勾选框
      [
        'enableCookieSwitch',
        'enableCookieConfirm',
        'disableWatermark',
        'enablePaginationDuplication',
        'updatePreviewCookie',
        'hideEmptyTitleEmail',
        'enableExternalImagePreview',
        'enableUpdateCheck',
        'enableAutoCookieRefresh',
        'enableAutoCookieRefreshToast',
        'interceptReplyFormUnvcode',
        'interceptReplyFormU200B',
        'interceptReplyFormAutoCompress',
        'enableSeamlessPaging',
        'enableAutoSeamlessPaging',
        'enableHDImageAndLayoutFix',
        'enableImageContextMenu',
        'enableThreadHistory',
        'enablePostHistory',
        'enableLinkBlank',
        'enableAutoUrlLinkify',
        'enableQuotePreview',
        'enableImageHideMode',
        'imageViewerSeparators',
        'extendQuote',
        'extendQuoteAvailabilityDetection',
        'enablePostExpandAll',
        'toggleSidebar',
        'disableAutoQuote',
      ].forEach(k=> $('#sp_'+k).prop('checked', this.state[k]));
      $('#sp_enableCustomDarkTheme').prop('checked', this.state.enableCustomDarkTheme === 'dark');
      // 深色开关视觉状态:日/月图标 + 轨道底色
      try { syncDarkToggleVisual(); } catch (e) {}
      // 二次确认饼干联动:快捷切换饼干关闭时禁用
      $('#sp_enableCookieConfirm').prop('disabled', !this.state.enableCookieSwitch);
      // 固定启用项:始终显示为开启
      $('#sp_enableImageHideMode').prop('checked', true);
      $('#sp_applyImageHideMode').val(this.state.applyImageHideMode || 'default');
      $('#sp_blockDisplayMode').val(this.state.blockDisplayMode || 'fold');
      $('#sp_threadCookieWhitelistDisplayMode').val(this.state.threadCookieWhitelistDisplayMode || 'fold');
      $('#sp_poAnnotationSideDisplayMode').val(this.state.poAnnotationSideDisplayMode || 'collapse');
      $('#sp_kaomojiSort').val(this.state.kaomojiSort || 'default');
      $('#sp_dockDisplayMode').val(this.state.dockDisplayMode || 'fixed');
      $('#sp_timeDisplayMode').val(this.state.timeDisplayMode === 'exact' ? 'exact' : 'relative');
      // 标记分组
      const groupsM = this.state.markedGroups.length ? this.state.markedGroups : [{desc:'',cookies:[]}];
      const $m = $('#marked-inputs-container').empty();
      groupsM.forEach((g, idx)=>{
        $m.append(spUi('buildCookieGroupTwoFieldRowHtml', 'marked', idx + 1, g));
      });
      // 屏蔽分组
      const groupsB = this.state.blockedCookies.length ? this.state.blockedCookies : [{desc:'',cookies:[]}];
      const $b = $('#blocked-inputs-container').empty();
      groupsB.forEach((g, idx)=>{
        $b.append(spUi('buildCookieGroupTwoFieldRowHtml', 'blocked', idx + 1, g));
      });
      const groupsW = this.state.threadCookieWhitelistGroups.length ? this.state.threadCookieWhitelistGroups : [{threads:[],cookies:[]}];
      const $w = $('#thread-cookie-whitelist-inputs-container').empty();
      groupsW.forEach((g, idx)=>{
        $w.append(spUi('buildThreadCookieWhitelistRowHtml', idx + 1, g));
      });
      const groupsK = spData('normalizeBlockedKeywordGroups', this.state.blockedKeywords);
      const $k = $('#blocked-keyword-inputs-container').empty();
      (groupsK.length ? groupsK : [{value:''}]).forEach((g, idx)=>{
        $k.append(spUi('buildBlockedKeywordGroupRowHtml', idx + 1, g));
      });
      const favoriteThreads = spData('normalizeFavoriteThreads', this.state.favoriteThreads);
      const $favoriteThreads = $('#favorite-thread-inputs-container').empty();
      (favoriteThreads.length ? favoriteThreads : [{desc:'', threadId:''}]).forEach((item, idx)=>{
        $favoriteThreads.append(spUi('buildFavoriteThreadRowHtml', idx + 1, item));
      });
      const subscriptionFeeds = Array.isArray(this.state.subscriptionFeeds) ? this.state.subscriptionFeeds : [];
      const $feeds = $('#subscription-feed-inputs-container').empty();
      (subscriptionFeeds.length ? subscriptionFeeds : [{desc:'', uuid:''}]).forEach((item, idx)=>{
        $feeds.append(spUi('buildSubscriptionFeedRowHtml', idx + 1, item));
      });
      $('#sp_replyModeDefault').val(this.state.replyModeDefault);
      $('#sp_replyExtraDefault').val(this.state.replyExtraDefault);
      // 初始折叠与按钮隐藏
      // 保存/同步时不再强制收起折叠区:模板本身已是 display:none + xdex-inv,
      // 若这里每次 hide/加 inv,会把用户刚展开的「标记饼干/屏蔽关键词」等分组立刻折回去。
      // $('.sp_fold_body').hide();
      // $('#btn_group_marked,#btn_sp_marked,#btn_group_blocked,#btn_sp_blocked,#btn_group_threadCookieWhitelist,#btn_sp_threadCookieWhitelist,#btn_group_blockedKeywords,#btn_sp_blockedKeywords,#btn_group_favoriteThreads,#btn_sp_favoriteThreads,#btn_group_subscriptionFeeds,#btn_sp_subscriptionFeeds,#btn_sp_importExport,#btn_sp_fullExport_reset,#btn_sp_fullExport_export,#btn_sp_fullExport_import').addClass('xdex-inv');
      $('#sp_replyModeDefault').val(this.state.replyModeDefault);
      $('#sp_replyExtraDefault').val(this.state.replyExtraDefault);
    }
  };

  /* --------------------------------------------------
   * tag 2. 回复编号
   * -------------------------------------------------- */
  // 数字样式:包裹为『n』
  const circledNumber = n => `『${n}』`;

  function updateReplyNumbers() {
    // 遍历每一个页面的回复区(含无缝加载的)
    $('.h-threads-item-replies').each(function () {
      let effectiveCount = 0;
      $(this).find('.h-threads-item-reply-icon').each(function () {
        const $reply = $(this).closest('[data-threads-id]');
        if ($reply.attr('data-threads-id') === '9999999') {
          // 特殊:小提示串号 -> 编号 0
          $(this).text(circledNumber(0));
        } else {
          // 普通回复 -> 依次递增
          effectiveCount++;
          $(this).text(circledNumber(effectiveCount));
        }
      });
    });
  }

  /* --------------------------------------------------
   * tag 3. 饼干标记 / 屏蔽 逻辑
   * -------------------------------------------------- */
  // 多分组标记时依次使用的背景色(可扩充)
  const markColors = [
    '#66CCFF','#00FFCC','#EE0000','#006666','#0080FF','#FFFF00',
    '#39C5BB','#9999FF','#FF4004','#3399FF','#D80000','#F6BE71',
    '#EE82EE','#FFA500','#FFE211','#FAAFBE','#0000FF'
  ];
  // 解析"最后一个冒号分隔"的分组:返回 {desc, list}
  function parseDescAndListByLastColon(raw) {
    const idx = Math.max(raw.lastIndexOf(':'), raw.lastIndexOf(':'));
    let desc = '';
    let cookiePart = '';
    if (idx > 0) {
      // 有冒号:冒号前是备注/说明,冒号后是饼干
      desc = raw.slice(0, idx).trim();
      cookiePart = raw.slice(idx + 1).trim();
    } else {
      // 没有冒号:整个字符串都是饼干
      cookiePart = raw.trim();
    }
    const list = Utils.strToList(cookiePart);
    return { desc, list };
  }
  // 校验分组说明长度(<=20 字符;满足“10个汉字/20个英文字符”的近似约束)
  function isValidDesc(desc) { return !desc || desc.length <= 20; }
  function isValidHexColor(color) {
    return /^#[0-9A-Fa-f]{6}$/.test(color);
  }
  function normalizeHexColor(color) {
    if (typeof color !== 'string') return '';
    const trimmed = color.trim();
    return isValidHexColor(trimmed) ? trimmed.toUpperCase() : '';
  }
  function clampColorChannel(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }
  function rgbToHex(rgb) {
    if (!rgb) return '';
    const r = clampColorChannel(Math.round(rgb.r || 0), 0, 255);
    const g = clampColorChannel(Math.round(rgb.g || 0), 0, 255);
    const b = clampColorChannel(Math.round(rgb.b || 0), 0, 255);
    return `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('').toUpperCase()}`;
  }
  function hexToRgb(color) {
    const normalized = normalizeHexColor(color);
    if (!normalized) return null;
    return {
      r: parseInt(normalized.slice(1, 3), 16),
      g: parseInt(normalized.slice(3, 5), 16),
      b: parseInt(normalized.slice(5, 7), 16),
    };
  }
  function hsvToRgb(h, s, v) {
    const hue = ((Number(h) % 360) + 360) % 360;
    const sat = clampColorChannel(Number(s), 0, 1);
    const val = clampColorChannel(Number(v), 0, 1);
    const c = val * sat;
    const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
    const m = val - c;
    let r1 = 0;
    let g1 = 0;
    let b1 = 0;
    if (hue < 60) {
      r1 = c; g1 = x;
    } else if (hue < 120) {
      r1 = x; g1 = c;
    } else if (hue < 180) {
      g1 = c; b1 = x;
    } else if (hue < 240) {
      g1 = x; b1 = c;
    } else if (hue < 300) {
      r1 = x; b1 = c;
    } else {
      r1 = c; b1 = x;
    }
    return {
      r: Math.round((r1 + m) * 255),
      g: Math.round((g1 + m) * 255),
      b: Math.round((b1 + m) * 255),
    };
  }
  function rgbToHsv(r, g, b) {
    const red = clampColorChannel(Number(r), 0, 255) / 255;
    const green = clampColorChannel(Number(g), 0, 255) / 255;
    const blue = clampColorChannel(Number(b), 0, 255) / 255;
    const max = Math.max(red, green, blue);
    const min = Math.min(red, green, blue);
    const delta = max - min;
    let h = 0;
    if (delta) {
      if (max === red) h = 60 * (((green - blue) / delta) % 6);
      else if (max === green) h = 60 * (((blue - red) / delta) + 2);
      else h = 60 * (((red - green) / delta) + 4);
    }
    if (h < 0) h += 360;
    return {
      h,
      s: max === 0 ? 0 : delta / max,
      v: max,
    };
  }
  function hexToHsv(color) {
    const rgb = hexToRgb(color);
    return rgb ? rgbToHsv(rgb.r, rgb.g, rgb.b) : { h: 0, s: 0, v: 1 };
  }
  function hsvToHex(h, s, v) {
    return rgbToHex(hsvToRgb(h, s, v));
  }
  function parseRgbColorString(value) {
    if (typeof value !== 'string') return null;
    const match = value.trim().match(/^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i);
    if (!match) return null;
    const r = Number(match[1]);
    const g = Number(match[2]);
    const b = Number(match[3]);
    if ([r, g, b].some((n) => Number.isNaN(n) || n < 0 || n > 255)) return null;
    return { r, g, b };
  }
  function formatRgbColor(rgb) {
    if (!rgb) return '';
    return `rgb(${clampColorChannel(Math.round(rgb.r || 0), 0, 255)}, ${clampColorChannel(Math.round(rgb.g || 0), 0, 255)}, ${clampColorChannel(Math.round(rgb.b || 0), 0, 255)})`;
  }
  function getMarkedGroupEffectiveColor(group, index) {
    return normalizeHexColor(group && group.color) || markColors[index % markColors.length];
  }

  function parseThreadCookieWhitelistRule(raw) {
    const idx = Math.max(raw.lastIndexOf(':'), raw.lastIndexOf(':'));
    let threadPart = '';
    let cookiePart = '';
    if (idx > 0) {
      threadPart = raw.slice(0, idx).trim();
      cookiePart = raw.slice(idx + 1).trim();
    } else {
      threadPart = raw.trim();
    }
    return {
      threads: Utils.strToList(threadPart),
      cookies: Utils.strToList(cookiePart),
    };
  }
  function buildThreadCookieWhitelistRowHtml(index, group = {}) {
    const desc = group.desc || '';
    const threadText = Array.isArray(group.threads) ? group.threads.join(',') : '';
    const cookieText = Array.isArray(group.cookies) ? group.cookies.join(',') : '';
    return `
      <div class="thread-cookie-whitelist-row" style="position:relative;margin:10px 0 8px;">
        <span style="position:absolute;top:-9px;left:10px;display:inline-block;padding:0 6px;font-size:12px;line-height:18px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;z-index:1;">#${index}</span>
        ${buildRowDragHandleHtml()}
        <button type="button" class="thread-cookie-whitelist-delete" style="position:absolute;top:-9px;right:10px;width:20px;height:20px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:1;">${XDEX_SVG_X}</button>
        <div style="display:grid;grid-template-columns:minmax(0,0.9fr) minmax(0,1.25fr) minmax(0,1.35fr);gap:8px;align-items:flex-start;border:1px solid #bfa58f;border-radius:6px;padding:12px 10px 10px 26px;background:rgba(255,255,255,0.18);box-sizing:border-box;width:100%;">
          <input class="thread-cookie-whitelist-desc-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="备注(可选)" value="${Utils.escapeHTML ? Utils.escapeHTML(desc) : desc}">
          <input class="thread-cookie-whitelist-threads-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="串号1,串号2" value="${Utils.escapeHTML ? Utils.escapeHTML(threadText) : threadText}">
          <input class="thread-cookie-whitelist-cookies-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="3-8位饼干ID1,饼干ID2" value="${Utils.escapeHTML ? Utils.escapeHTML(cookieText) : cookieText}">
        </div>
      </div>`;
  }
  function buildCookieGroupRowHtml(type, index, value, placeholder) {
    return `
      <div class="${type}-row" style="position:relative;margin:10px 0 8px;">
        <span style="position:absolute;top:-9px;left:10px;display:inline-block;padding:0 6px;font-size:12px;line-height:18px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;z-index:1;">#${index}</span>
        ${buildRowDragHandleHtml()}
        <button type="button" class="${type}-delete" style="position:absolute;top:-9px;right:10px;width:20px;height:20px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:1;">${XDEX_SVG_X}</button>
        <div style="border:1px solid #bfa58f;border-radius:6px;padding:12px 10px 10px 26px;background:rgba(255,255,255,0.18);box-sizing:border-box;width:100%;">
          <input class="${type}-input" style="width:100%;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="${placeholder}" value="${Utils.escapeHTML ? Utils.escapeHTML(value || '') : (value || '')}">
        </div>
      </div>`;
  }
  function getBlockedKeywordPlaceholder(regexOn) {
    return regexOn
      ? '正则模式:整组作为一条正则,如 foo|bar 或 (串|回复)\\d+'
      : '关键词1,关键词2;7/8/9位数字同时也作为串号/回复号匹配';
  }
  function buildBlockedKeywordGroupRowHtml(index, group = {}) {
    const keywordText = typeof group.value === 'string'
      ? group.value
      : (Array.isArray(group.keywords) ? group.keywords.join(',') : '');
    const regexOn = isBlockedKeywordRegexGroup(group);
    const placeholder = getBlockedKeywordPlaceholder(regexOn);
    const checkedAttr = regexOn ? ' checked' : '';
    const safeValue = Utils.escapeHTML ? Utils.escapeHTML(keywordText || '') : (keywordText || '');
    const safePlaceholder = Utils.escapeHTML ? Utils.escapeHTML(placeholder) : placeholder;
    // 右侧开关样式对齐设置页 xdex-switch;默认关闭=普通逗号关键词,开启=整组正则
    return `
      <div class="blocked-keyword-row" style="position:relative;margin:10px 0 8px;" data-regex="${regexOn ? '1' : '0'}">
        <span style="position:absolute;top:-9px;left:10px;display:inline-block;padding:0 6px;font-size:12px;line-height:18px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;z-index:1;">#${index}</span>
        ${buildRowDragHandleHtml()}
        <button type="button" class="blocked-keyword-delete" style="position:absolute;top:-9px;right:10px;width:20px;height:20px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:1;">${XDEX_SVG_X}</button>
        <div style="border:1px solid #bfa58f;border-radius:6px;padding:12px 10px 10px 26px;background:rgba(255,255,255,0.18);box-sizing:border-box;width:100%;">
          <div style="display:flex;align-items:center;gap:8px;">
            <input class="blocked-keyword-input" style="flex:1;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="${safePlaceholder}" value="${safeValue}">
            <div style="display:flex;align-items:center;gap:6px;flex:0 0 auto;white-space:nowrap;">
              <input type="checkbox" id="blocked-keyword-regex-${index}" class="xdex-switch blocked-keyword-regex" role="switch"${checkedAttr}>
              <label for="blocked-keyword-regex-${index}" style="font-size:12px;color:#6f5d50;user-select:none;">正则</label>
            </div>
          </div>
        </div>
      </div>`;
  }
  // ===== 分组顺序传播(拖拽排序作为可传播设置) =====
  // 设计:state.groupOrderMeta[分组字段] = { order: [语义键...], at: 时间戳 }
  //  - 语义键与该分组合并函数的匹配键一致(双键:desc 优先,cookies 兜底)
  //  - 合并规则:两端都有则 at 新者胜;order 中不存在的项追加末尾;已失效的键丢弃
  //  - 静默传播:groupOrderMeta 在 WEBDAV_SETTINGS_SKIP_KEYS 中,不触发冲突弹窗
  const GROUP_ORDER_FIELDS = ['markedGroups', 'blockedCookies', 'threadCookieWhitelistGroups', 'blockedKeywords', 'favoriteThreads', 'subscriptionFeeds'];
  // 分组类型 → 键提取(与合并函数保持一致)
  function groupOrderKeysOf(field, item) {
    if (!item || typeof item !== 'object') return [];
    const keys = [];
    const desc = String(item.desc || item.name || '').trim();
    if (desc) keys.push('d:' + desc);
    // 各分组的存储结构(与 collect/save 函数写入的字段一致):
    //   markedGroups/blockedCookies: {desc, color?, cookies:[...]}
    //   threadCookieWhitelistGroups: {desc, threads:[...], cookies:[...]}
    //   blockedKeywords:             {value, regex}
    //   favoriteThreads:             {desc, threadId}
    //   subscriptionFeeds:           {desc, uuid}
    const joinList = (v) => (Array.isArray(v) ? v : String(v == null ? '' : v).split(/\s*,\s*/))
      .map((x) => String(x).trim()).filter(Boolean).join(',');
    if (field === 'markedGroups' || field === 'blockedCookies') {
      const ck = joinList(item.cookies);
      if (ck) keys.push('c:' + ck);
    } else if (field === 'threadCookieWhitelistGroups') {
      const th = joinList(item.threads);
      if (th) keys.push('t:' + th);
    } else if (field === 'favoriteThreads') {
      const tid = String(item.threadId || '').trim();
      if (tid) keys.push('i:' + tid);
    } else if (field === 'subscriptionFeeds') {
      const uuid = String(item.uuid || '').trim();
      if (uuid) keys.push('u:' + uuid);
    } else if (field === 'blockedKeywords') {
      const v = String(item.value || '').trim();
      if (v) keys.push('k:' + (item.regex ? 'r:' : '') + v);
    }
    return keys;
  }
  // 数组 → 键序列(取每项的首个可用键,用于写入 order)
  function groupOrderSnapshot(field, arr) {
    const out = [];
    (Array.isArray(arr) ? arr : []).forEach((item) => {
      const keys = groupOrderKeysOf(field, item);
      if (keys.length) out.push(keys[0]);
    });
    return out;
  }
  // 按 order 键序列重排数组:命中的按 order 排序,未命中的追加末尾(保持原相对顺序)
  function applyGroupOrder(field, arr, order) {
    const list = Array.isArray(arr) ? arr.slice() : [];
    if (!Array.isArray(order) || !order.length) return list;
    const used = new Set();
    const result = [];
    order.forEach((key) => {
      for (let i = 0; i < list.length; i++) {
        if (used.has(i)) continue;
        const keys = groupOrderKeysOf(field, list[i]);
        // 双键匹配:order 中的键命中该项任一键即可(兼容「后补备注」后键变化)
        if (keys.indexOf(key) !== -1) { used.add(i); result.push(list[i]); return; }
      }
    });
    list.forEach((item, i) => { if (!used.has(i)) result.push(item); });
    return result;
  }
  // 合并两端顺序元数据:at 新者胜;返回 {order, at} 或 null(两端都无)
  function mergeGroupOrderMeta(localMeta, remoteMeta) {
    const l = localMeta && typeof localMeta === 'object' ? localMeta : null;
    const r = remoteMeta && typeof remoteMeta === 'object' ? remoteMeta : null;
    if (!l && !r) return null;
    if (!l) return r;
    if (!r) return l;
    const lat = Number(l.at) || 0;
    const rat = Number(r.at) || 0;
    if (rat > lat) return r;
    if (lat > rat) return l;
    // 时间相同:优先更长(覆盖更多项)的序列,保证结果稳定
    const lo = Array.isArray(l.order) ? l.order : [];
    const ro = Array.isArray(r.order) ? r.order : [];
    return ro.length > lo.length ? r : l;
  }
  // 对六个分组字段统一应用顺序合并(local/remote 均为 settings 对象)
  function mergeGroupOrdersInto(result, localSettings, remoteSettings) {
    const localMeta = (localSettings && localSettings.groupOrderMeta) || {};
    const remoteMeta = (remoteSettings && remoteSettings.groupOrderMeta) || {};
    const mergedMeta = {};
    GROUP_ORDER_FIELDS.forEach((field) => {
      const picked = mergeGroupOrderMeta(localMeta[field], remoteMeta[field]);
      if (picked) mergedMeta[field] = picked;
      if (result[field] && picked) {
        result[field] = applyGroupOrder(field, result[field], picked.order);
      }
    });
    if (Object.keys(mergedMeta).length) result.groupOrderMeta = mergedMeta;
  }

  // ===== 分组行拖拽排序(标记/屏蔽饼干、只看饼干、屏蔽关键词、常用串、订阅号共用) =====
  // 设计:
  //  - 手柄只在行左上角「六点」区域响应,输入框/色块/删除按钮完全不受影响;
  //  - 拖动中实时 insertBefore 重排(视觉即时反馈),松手后重编号徽标并调 save 回调落盘;
  //  - save 失败(校验不过)时回调返回 false → 调用方负责 syncInputs() 回滚到 state 顺序;
  //  - 与 beginMarkedColorDrag 并存:事件命名空间独立(.rowDrag vs .markedColorDrag)。
  function buildRowDragHandleHtml() {
    return `<span class="xdex-row-drag-handle" title="拖动排序" style="position:absolute;left:4px;top:50%;transform:translateY(-50%);width:14px;height:22px;display:flex;align-items:center;justify-content:center;cursor:grab;z-index:2;color:#8a7566;opacity:.8;user-select:none;touch-action:none;">${XDEX_SVG_GRIP}</span>`;
  }
  // 行内序号徽标重写(拖动后调用;仅改显示文本,存储中无序号字段)
  function renumberRowBadges($container, rowSelector) {
    $container.find(rowSelector).each(function (idx) {
      const $row = $(this);
      const $badge = $row.children('span').first();
      if ($badge.length && /^#\d+$/.test(($badge.text() || '').trim())) $badge.text('#' + (idx + 1));
    });
  }
  // 从行 DOM 提取语义键(与 groupOrderKeysOf 的格式一致:d:/c:/t:/i:/u:/k:)
  // 用于拖拽结束时按新 DOM 顺序记录 groupOrderMeta(此刻 state 仍是旧顺序)
  // class 名与各 collect 函数读取的输入框保持一致
  function rowOrderKeyFromDom(field, $row) {
    try {
      let desc = '';
      if (field === 'markedGroups') desc = String($row.find('.marked-desc-input').val() || '').trim();
      else if (field === 'blockedCookies') desc = String($row.find('.blocked-desc-input').val() || '').trim();
      else if (field === 'threadCookieWhitelistGroups') desc = String($row.find('.thread-cookie-whitelist-desc-input').val() || '').trim();
      else if (field === 'blockedKeywords') desc = String($row.find('.blocked-keyword-desc-input').val() || '').trim();
      else if (field === 'favoriteThreads') desc = String($row.find('.favorite-thread-desc-input').val() || '').trim();
      else if (field === 'subscriptionFeeds') desc = String($row.find('.subscription-feed-desc-input').val() || '').trim();
      if (desc) return 'd:' + desc;
      if (field === 'markedGroups' || field === 'blockedCookies') {
        const sel = field === 'markedGroups' ? '.marked-cookies-input' : '.blocked-cookies-input';
        const ck = String($row.find(sel).val() || '').trim().split(/\s*,\s*/).filter(Boolean).join(',');
        if (ck) return 'c:' + ck;
      } else if (field === 'threadCookieWhitelistGroups') {
        const t = String($row.find('.thread-cookie-whitelist-threads-input').val() || '').trim().split(/\s*,\s*/).filter(Boolean).join(',');
        if (t) return 't:' + t;
      } else if (field === 'favoriteThreads') {
        // 与 collectFavoriteThreadsFromPanel 一致:用规范化后的 threadId(DOM 里是原始输入)
        const raw = String($row.find('.favorite-thread-id-input').val() || '').trim();
        let tid = raw;
        try { if (typeof normalizeFavoriteThreadInput === 'function') tid = normalizeFavoriteThreadInput(raw) || raw; } catch (e) {}
        if (tid) return 'i:' + tid;
      } else if (field === 'subscriptionFeeds') {
        const u = String($row.find('.subscription-feed-uuid-input').val() || '').trim();
        if (u) return 'u:' + u;
      } else if (field === 'blockedKeywords') {
        const v = String($row.find('.blocked-keyword-input').val() || '').trim();
        const isRegex = $row.attr('data-regex') === '1';
        if (v) return 'k:' + (isRegex ? 'r:' : '') + v;
      }
    } catch (e) {}
    return null;
  }
  // 通用拖拽安装:containerSelector 内 rowSelector 行按手柄拖动排序
  // field:    分组字段名(写入 state.groupOrderMeta[field] 供跨端传播顺序)
  // onSorted: 排序落盘回调,返回 false 表示失败(由调用方回滚)
  function installRowDragSorting({ containerSelector, rowSelector, field, onSorted }) {
    const $container = $(containerSelector);
    if (!$container.length) return;
    $container.off('mousedown.rowDrag touchstart.rowDrag', '.xdex-row-drag-handle')
      .on('mousedown.rowDrag touchstart.rowDrag', '.xdex-row-drag-handle', function (startEvent) {
        const isTouch = startEvent.type.indexOf('touch') === 0;
        if (!isTouch && startEvent.button !== 0) return; // 仅左键
        startEvent.preventDefault();
        startEvent.stopPropagation();
        const $handle = $(this);
        const $row = $handle.closest(rowSelector);
        if (!$row.length || $container.find(rowSelector).length < 2) return; // 单行无需排序
        const rowHeight = $row.outerHeight(true);
        const dragOffsetY = (isTouch
          ? (startEvent.originalEvent.touches[0].clientY)
          : startEvent.clientY) - $row[0].getBoundingClientRect().top;
        $row.css({ opacity: 0.55, boxShadow: '0 6px 16px rgba(0,0,0,0.25)' });
        $handle.css('cursor', 'grabbing');
        $('body').css('user-select', 'none');

        const moveNs = isTouch ? 'touchmove.rowDrag' : 'mousemove.rowDrag';
        const endNs = isTouch ? 'touchend.rowDrag touchcancel.rowDrag' : 'mouseup.rowDrag';
        const getPoint = (evt) => {
          const oe = evt.originalEvent || evt;
          const touch = oe.touches && oe.touches[0];
          const changed = oe.changedTouches && oe.changedTouches[0];
          return touch || changed || oe;
        };
        const onMove = (evt) => {
          evt.preventDefault();
          const point = getPoint(evt);
          const y = point.clientY;
          // 以「被拖行中点」判定目标插入位置
          const draggedMid = y - dragOffsetY + rowHeight / 2;
          let $target = null;
          $container.find(rowSelector).each(function () {
            if (this === $row[0]) return;
            const rect = this.getBoundingClientRect();
            if (draggedMid < rect.top + rect.height / 2) { $target = $(this); return false; }
          });
          if ($target && $target.length) {
            if ($target[0] !== $row[0].nextElementSibling) $target.before($row);
          } else {
            $container.append($row);
          }
        };
        const onEnd = () => {
          $(document).off('.rowDrag');
          $row.css({ opacity: '', boxShadow: '' });
          $handle.css('cursor', 'grab');
          $('body').css('user-select', '');
          renumberRowBadges($container, rowSelector);
          // 先按新 DOM 顺序记录 groupOrderMeta(save 成功后即随设置落盘;失败则随后回滚)
          let wroteOrder = false;
          if (field && typeof SettingPanel !== 'undefined' && SettingPanel.state) {
            try {
              // ★ 必须从 DOM 提取(此刻 state[field] 仍是旧顺序);
              //   每行的语义键从行内输入框读取,与 groupOrderKeysOf 的键格式保持一致
              const order = [];
              $container.find(rowSelector).each(function () {
                const key = rowOrderKeyFromDom(field, $(this));
                if (key) order.push(key);
              });
              if (order.length) {
                if (!SettingPanel.state.groupOrderMeta || typeof SettingPanel.state.groupOrderMeta !== 'object') {
                  SettingPanel.state.groupOrderMeta = {};
                }
                SettingPanel.state.groupOrderMeta[field] = { order, at: Date.now() };
                wroteOrder = true;
              }
            } catch (e) {}
          }
          if (typeof onSorted === 'function') {
            const ok = onSorted();
            if (ok === false) {
              // 校验失败:save 未写入也未重绘 → 回滚 DOM 与顺序元数据到 state 顺序
              if (wroteOrder && field && SettingPanel.state.groupOrderMeta) {
                try { delete SettingPanel.state.groupOrderMeta[field]; } catch (e) {}
              }
              try { SettingPanel.syncInputs(); } catch (e) {}
            }
          }
        };
        $(document).off('.rowDrag').on(moveNs, onMove).on(endNs, onEnd);
      });
  }
  function buildCookieGroupTwoFieldRowHtml(type, index, group = {}) {
    const desc = group.desc || '';
    const cookieText = Array.isArray(group.cookies) ? group.cookies.join(',') : '';
    const color = normalizeHexColor(group.color);
    const effectiveColor = type === 'marked' ? getMarkedGroupEffectiveColor(group, index - 1) : '';
    const gridTemplateColumns = type === 'marked'
      ? 'minmax(0,2fr) minmax(0,3fr) 34px'
      : 'minmax(0,2fr) minmax(0,3fr)';
    const markedSwatchHtml = type === 'marked' ? `
          <div class="marked-color-cell" style="position:relative;min-width:0;">
            <input type="hidden" class="marked-color-value" value="${Utils.escapeHTML ? Utils.escapeHTML(color) : color}">
            <button
              type="button"
              class="marked-color-swatch"
              aria-label="设置标记颜色"
              data-default-color="${effectiveColor}"
              style="width:100%;height:31px;padding:0;border:1px solid #a98f7a;border-radius:8px;box-sizing:border-box;background:${effectiveColor};cursor:pointer;box-shadow:inset 0 0 0 1px rgba(255,255,255,0.42);"
            ></button>
            <div class="marked-color-popover" style="display:none;position:absolute;right:0;z-index:12;width:220px;padding:10px;border:1px solid #bfa58f;border-radius:10px;background:var(--xdex-sp-panel-bg);box-shadow:0 8px 18px var(--xdex-sp-shadow);">
              <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
                <div class="marked-color-preview" style="width:30px;height:30px;flex:0 0 30px;border:1px solid #a98f7a;border-radius:8px;background:${effectiveColor};box-shadow:inset 0 0 0 1px rgba(255,255,255,0.42);"></div>
                <div style="min-width:0;flex:1;">
                  <div class="marked-color-status" style="font-size:12px;line-height:1.25;color:#6f5d50;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">#RRGGBB,自定义为空时按默认序列</div>
                  <div class="marked-color-default-hint" style="font-size:11px;line-height:1.25;color:#8a7768;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">默认 ${effectiveColor}</div>
                </div>
              </div>
              <div class="marked-color-sv" style="position:relative;height:110px;border:1px solid #a98f7a;border-radius:8px;cursor:crosshair;overflow:hidden;background:linear-gradient(to top, #000 0%, transparent 100%), linear-gradient(to right, #fff 0%, hsl(0,100%,50%) 100%);margin-bottom:8px;">
                <div class="marked-color-sv-thumb" style="position:absolute;left:100%;top:0;width:12px;height:12px;margin-left:-6px;margin-top:-6px;border:2px solid #fff;border-radius:50%;box-shadow:0 0 0 1px rgba(0,0,0,0.35);pointer-events:none;background:transparent;"></div>
              </div>
              <div class="marked-color-hue" style="position:relative;height:14px;border:1px solid #a98f7a;border-radius:999px;cursor:pointer;overflow:hidden;background:linear-gradient(to right, #FF0000 0%, #FFFF00 16.66%, #00FF00 33.33%, #00FFFF 50%, #0000FF 66.66%, #FF00FF 83.33%, #FF0000 100%);margin-bottom:8px;">
                <div class="marked-color-hue-thumb" style="position:absolute;left:0;top:50%;width:12px;height:12px;margin-left:-6px;margin-top:-6px;border:2px solid #fff;border-radius:50%;box-sizing:border-box;box-shadow:0 0 0 1px rgba(0,0,0,0.35);pointer-events:none;background:transparent;"></div>
              </div>
              <div style="display:flex;gap:6px;margin-bottom:8px;">
                <button type="button" class="marked-color-format" data-format="hex" style="flex:1;padding:4px 0;border:1px solid #7da6bf;border-radius:8px;background:#66CCFF;color:#fff;cursor:pointer;">HEX</button>
                <button type="button" class="marked-color-format" data-format="rgb" style="flex:1;padding:4px 0;border:1px solid #a98f7a;border-radius:8px;background:#F0E0D6;color:#6f5d50;cursor:pointer;">RGB</button>
              </div>
              <input class="marked-color-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;margin-bottom:8px;" placeholder="#66CCFF" value="${Utils.escapeHTML ? Utils.escapeHTML(color) : color}">
              <div style="display:flex;gap:6px;">
                <button type="button" class="marked-color-clear" style="flex:1;padding:4px 0;border:1px solid #a98f7a;border-radius:8px;background:#F0E0D6;cursor:pointer;">恢复默认</button>
                <button type="button" class="marked-color-save" style="flex:1;padding:4px 0;border:1px solid #7da6bf;border-radius:8px;background:#66CCFF;color:#fff;cursor:pointer;">保存</button>
              </div>
            </div>
          </div>` : '';
    return `
      <div class="${type}-row" style="position:relative;margin:10px 0 8px;">
        <span style="position:absolute;top:-9px;left:10px;display:inline-block;padding:0 6px;font-size:12px;line-height:18px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;z-index:1;">#${index}</span>
        ${buildRowDragHandleHtml()}
        <button type="button" class="${type}-delete" style="position:absolute;top:-9px;right:10px;width:20px;height:20px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:1;">${XDEX_SVG_X}</button>
        <div style="display:grid;grid-template-columns:${gridTemplateColumns};gap:8px;align-items:flex-start;border:1px solid #bfa58f;border-radius:6px;padding:12px 10px 10px 26px;background:rgba(255,255,255,0.18);box-sizing:border-box;width:100%;">
          <input class="${type}-desc-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="备注(可选)" value="${Utils.escapeHTML ? Utils.escapeHTML(desc) : desc}">
          <input class="${type}-cookies-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="3-8位饼干ID1,饼干ID2" value="${Utils.escapeHTML ? Utils.escapeHTML(cookieText) : cookieText}">
${markedSwatchHtml}
        </div>
      </div>`;
  }
  function normalizeThreadCookieWhitelistGroups(val) {
    if (!Array.isArray(val)) return [];
    return val.map((g) => ({
      desc: typeof g.desc === 'string' && isValidDesc(g.desc.trim()) ? g.desc.trim() : '',
      threads: Array.isArray(g.threads) ? [...new Set(g.threads.filter(isValidThreadId))] : [],
      cookies: Array.isArray(g.cookies) ? [...new Set(g.cookies.filter(Utils.cookieLegal))] : [],
    })).filter((g) => g.threads.length && g.cookies.length);
  }
  function mergeThreadCookieWhitelistGroups(groups) {
    const threadOrder = [];
    const threadToState = new Map();
    const mergeEvents = [];
    groups.forEach((group, idx) => {
      const desc = typeof group.desc === 'string' && isValidDesc(group.desc.trim()) ? group.desc.trim() : '';
      const cookies = [...new Set((group.cookies || []).filter(Utils.cookieLegal))];
      const threads = [...new Set((group.threads || []).filter(isValidThreadId))];
      threads.forEach((threadId) => {
        if (!threadToState.has(threadId)) {
          threadToState.set(threadId, { desc, cookies: new Set(), firstIndex: typeof group.rowIndex === 'number' ? group.rowIndex : idx });
          threadOrder.push(threadId);
        } else {
          mergeEvents.push({
            threadId,
            rowIndex: typeof group.rowIndex === 'number' ? group.rowIndex : idx,
            desc,
            cookies,
          });
        }
        cookies.forEach((cookie) => threadToState.get(threadId).cookies.add(cookie));
      });
    });
    const grouped = new Map();
    const mergedGroups = [];
    threadOrder.forEach((threadId) => {
      const state = threadToState.get(threadId);
      const cookies = Array.from(state.cookies);
      const desc = state.desc || '';
      const key = `${desc}\u0002${cookies.slice().sort().join('\u0001')}`;
      if (!grouped.has(key)) {
        const group = { desc, threads: [], cookies };
        grouped.set(key, group);
        mergedGroups.push(group);
      }
      grouped.get(key).threads.push(threadId);
    });
    return {
      groups: mergedGroups,
      mergeEvents,
    };
  }
  // 兼容旧版本 blockedCookies 值到“组结构”
  function normalizeBlockedGroups(val) {
    if (!val) return [];
    if (typeof val === 'string') {
      const tokens = Utils.strToList(val);
      return tokens.map(t=>{
        const {desc, list} = parseDescAndListByLastColon(t);
        const id = list[0] || '';
        return id && Utils.cookieLegal(id) ? { desc, cookies:[id] } : null;
      }).filter(Boolean);
    }
    if (Array.isArray(val)) {
      if (val.length && 'cookies' in val[0]) {
        return val.map(g=>({
          desc: g.desc || '',
          cookies: Array.isArray(g.cookies) ? g.cookies.filter(Utils.cookieLegal) : []
        })).filter(g=>g.cookies.length);
      }
      if (val.length && 'cookie' in val[0]) {
        const map = new Map();
        val.forEach(({cookie, desc})=>{
          if (!Utils.cookieLegal(cookie)) return;
          const key = desc || '';
          if (!map.has(key)) map.set(key, []);
          map.get(key).push(cookie);
        });
        return [...map.entries()].map(([desc, cookies])=>({desc, cookies}));
      }
    }
    return [];
  }
  function escapeBlockedKeywordInputToken(keyword) {
    return String(keyword || '').trim().replace(/([\\,,])/g, '\\$1');
  }
  function joinBlockedKeywordInputTokens(keywords) {
    return keywords.map(escapeBlockedKeywordInputToken).filter(Boolean).join(',');
  }
  function normalizeBlockedKeywordGroupValue(group) {
    if (typeof group === 'string') return group.trim();
    if (Array.isArray(group)) return joinBlockedKeywordInputTokens(group);
    if (!group || typeof group !== 'object') return '';
    if (typeof group.value === 'string') return group.value.trim();
    if (typeof group.text === 'string') return group.text.trim();
    if (typeof group.keywords === 'string') return group.keywords.trim();
    if (Array.isArray(group.keywords)) return joinBlockedKeywordInputTokens(group.keywords);
    return '';
  }
  function isBlockedKeywordRegexGroup(group) {
    return !!(group && typeof group === 'object' && (group.regex === true || group.isRegex === true || group.useRegex === true));
  }
  function normalizeBlockedKeywordGroups(val) {
    if (!val) return [];
    if (typeof val === 'string') {
      const value = normalizeBlockedKeywordGroupValue(val);
      return Utils.strToList(value).length ? [{ value, regex: false }] : [];
    }
    if (!Array.isArray(val)) return [];
    return val.map((group) => {
      const value = normalizeBlockedKeywordGroupValue(group);
      const regex = isBlockedKeywordRegexGroup(group);
      return { value, regex };
    }).filter((group) => {
      if (!group.value) return false;
      // 正则组:整段 pattern 原样保留;普通组:仍按逗号切词后判断是否为空
      return group.regex ? true : Utils.strToList(group.value).length > 0;
    });
  }
  // 仅扁平化“普通关键词组”(正则组不参与逗号切分)
  function flattenBlockedKeywords(groups) {
    return [...new Set(
      normalizeBlockedKeywordGroups(groups)
        .filter((group) => !group.regex)
        .flatMap((group) => Utils.strToList(group.value))
    )];
  }
  // 编译屏蔽关键词正则;非法 pattern 返回 null(匹配时跳过,保存时再提示)
  function compileBlockedKeywordRegex(pattern) {
    const source = String(pattern || '').trim();
    if (!source) return null;
    try {
      // 正文 textContent 常带前导/段落换行;默认 ignoreCase + dotAll,让 .* 可跨行
      // 不用 m:^/$ 仍表示整段正文起止,而不是每一行
      return new RegExp(source, 'is');
    } catch (e) {
      try {
        // 极旧环境无 s 标志时回退到仅 i
        return new RegExp(source, 'i');
      } catch (e2) {
        return null;
      }
    }
  }
  // 7/8/9 位纯数字关键词:除正文子串外,也按串号/回复号做 ID 匹配
  function isThreadIdKeyword(keyword) {
    return /^\d{7,9}$/.test(String(keyword || '').trim());
  }
  // 兼容旧名
  function isEightDigitKeyword(keyword) { return isThreadIdKeyword(keyword); }
  function normalizeMarkedGroups(val) {
    if (!val) return [];
    if (typeof val === 'string') {
      const tokens = Utils.strToList(val);
        return tokens.map(t => {
          const { desc, list } = parseDescAndListByLastColon(t);
          const cookies = list.filter(Utils.cookieLegal);
          return cookies.length ? { desc, color: '', colorAt: 0, cookies } : null;
        }).filter(Boolean);
      }
      if (Array.isArray(val)) {
        if (val.length && 'cookies' in val[0]) {
          return val.map(g => ({
            desc: typeof g.desc === 'string' && isValidDesc(g.desc.trim()) ? g.desc.trim() : '',
            color: normalizeHexColor(g.color),
            // colorAt:自定义配色的修改时间戳(0 = 旧版本数据/从未手动设置过配色)
            colorAt: Math.max(0, Number(g.colorAt) || 0),
            cookies: Array.isArray(g.cookies) ? [...new Set(g.cookies.filter(Utils.cookieLegal))] : []
          })).filter(g => g.cookies.length);
        }
      if (val.length && 'cookie' in val[0]) {
        const map = new Map();
        val.forEach(({ cookie, desc }) => {
          if (!Utils.cookieLegal(cookie)) return;
          const key = typeof desc === 'string' && isValidDesc(desc.trim()) ? desc.trim() : '';
          if (!map.has(key)) map.set(key, []);
          map.get(key).push(cookie);
        });
        return [...map.entries()].map(([desc, cookies]) => ({ desc, color: '', colorAt: 0, cookies: [...new Set(cookies)] }));
      }
    }
    return [];
  }

  // 配色合并规则(标记饼干跨端共用):
  //  - 未手动设置过(color='' 且无时间戳)→ 视为“保持默认”,对方任何变更都覆盖
  //  - 仅一方手动设置 → 该方生效
  //  - 双方都手动设置 → colorAt 新者胜;同时间/都无时间戳(旧版本数据)→ 本地优先
  //  - 清除配色(color='' 但带 colorAt)视为一次手动变更,可覆盖更旧的配色
  // 返回 null 表示该对象不是标记分组(如屏蔽饼干),不应注入 color 字段
  function resolveMarkedGroupColorState(localGroup, remoteGroup) {
    const lHas = localGroup && typeof localGroup === 'object' && 'color' in localGroup;
    const rHas = remoteGroup && typeof remoteGroup === 'object' && 'color' in remoteGroup;
    if (!lHas && !rHas) return null;
    const lc = normalizeHexColor(localGroup && localGroup.color);
    const rc = normalizeHexColor(remoteGroup && remoteGroup.color);
    const la = Math.max(0, Number(localGroup && localGroup.colorAt) || 0);
    const ra = Math.max(0, Number(remoteGroup && remoteGroup.colorAt) || 0);
    const lManual = !!lc || la > 0;
    const rManual = !!rc || ra > 0;
    if (!lManual && !rManual) return { color: '', colorAt: 0 };
    if (!lManual) return { color: rc, colorAt: ra };
    if (!rManual) return { color: lc, colorAt: la };
    return ra > la ? { color: rc, colorAt: ra } : { color: lc, colorAt: la };
  }

  // 标记:支持同一饼干命中多个分组时,title 展示多行备注,颜色取首匹配组
  function extractCookieIdFromUidElement(el, fallbackText) {
    const datasetId = el && el.dataset ? el.dataset.xdexCookieId : '';
    if (datasetId) return datasetId;
    const font = el && el.querySelector ? el.querySelector('font') : null;
    if (font) {
      for (const node of font.childNodes) {
        if (node.nodeType === Node.TEXT_NODE) {
          const value = String(node.nodeValue || '').trim();
          if (value) return value;
        }
      }
    }
    const value = (String(fallbackText || '').split(':')[1] || fallbackText || '').trim();
    const match = String(value).match(/[A-Za-z0-9]{3,8}/);
    return match ? match[0] : value;
  }
  function wrapCookieMarkTargetPreserveHtml(el, color) {
    if (!el) return;
    const existing = el.querySelector('.xdex-cookie-mark-target');
    if (existing) {
      $(existing).css({ background: color, padding:'0 3px', borderRadius:'2px' });
      return;
    }
    const target = document.createElement('span');
    target.className = 'xdex-cookie-mark-target';
    $(target).css({ background: color, padding:'0 3px', borderRadius:'2px' });
    let passedColon = false;
    Array.from(el.childNodes).forEach(node => {
      if (!passedColon && node.nodeType === Node.TEXT_NODE) {
        const value = node.nodeValue || '';
        const index = value.indexOf(':');
        if (index !== -1) {
          node.nodeValue = value.slice(0, index + 1);
          const rest = value.slice(index + 1);
          if (rest) target.appendChild(document.createTextNode(rest));
          passedColon = true;
          return;
        }
      }
      if (passedColon) target.appendChild(node);
    });
    if (!target.childNodes.length) {
      while (el.firstChild) target.appendChild(el.firstChild);
    }
    el.appendChild(target);
  }
  function markAllCookies(groups, root) {
    const $scope = root ? $(root).find('span.h-threads-info-uid').add($(root).filter('span.h-threads-info-uid')) : $('span.h-threads-info-uid');
    $scope.each(function(){
      const $el = $(this);
      const rawText = $el.text();
      const cid = extractCookieIdFromUidElement(this, rawText);
      // 先清除旧的标记样式
      $el.find('.xdex-cookie-mark-target').contents().unwrap();
      $el.css({ background: '', padding: '', borderRadius: '' }).removeAttr('title');
      // 收集所有匹配的分组索引和备注
      let firstMatchIdx = -1;
      const hits = [];
      for (let i=0; i<groups.length; i++){
        const g = groups[i];
        if (g.cookies.some(p=>Utils.cookieMatch(cid,p))) {
          if (firstMatchIdx === -1) firstMatchIdx = i; // 记录第一个匹配的分组索引
          if (g.desc) hits.push(g.desc); // 只有有备注时才加入 hits
        }
      }
      // 如果没有匹配到任何分组,保持清除状态
      if (firstMatchIdx === -1) return;
      // 根据第一个匹配的分组索引选择颜色
      const color = getMarkedGroupEffectiveColor(groups[firstMatchIdx], firstMatchIdx);
      if (rawText.includes(':') && cid) {
        wrapCookieMarkTargetPreserveHtml(this, color);
      } else {
        $el.css({ background: color, padding:'0 3px', borderRadius:'2px' });
      }
      // 只有当有备注时才设置 title
      if (hits.length > 0) {
        $el.attr('title', hits.join('\n'));
      }
    });
  }
  function getFilterConfig(cfg) {
    if (cfg && typeof cfg === 'object') {
      const next = Object.assign({}, cfg);
      next.blockedCookies = normalizeBlockedGroups(next.blockedCookies);
      next.blockedKeywords = normalizeBlockedKeywordGroups(next.blockedKeywords);
      next.threadCookieWhitelistGroups = normalizeThreadCookieWhitelistGroups(next.threadCookieWhitelistGroups);
      return next;
    }
    try {
      const next = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      next.markedGroups = normalizeMarkedGroups(next.markedGroups);
      next.blockedCookies = normalizeBlockedGroups(next.blockedCookies);
      next.blockedKeywords = normalizeBlockedKeywordGroups(next.blockedKeywords);
      next.threadCookieWhitelistGroups = normalizeThreadCookieWhitelistGroups(next.threadCookieWhitelistGroups);
      return next;
    } catch (e) {
      return Object.assign({}, SettingPanel.defaults);
    }
  }
  function isUpdateCheckEnabled() {
    try {
      const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      return !!cfg.enableUpdateCheck;
    } catch (e) {
      return !!SettingPanel.defaults.enableUpdateCheck;
    }
  }
  function asFilterRoot(root) {
    if (!root) return $(document);
    return root.jquery ? root : $(root);
  }
  function withSelf($root, selector) {
    return $root.filter(selector).add($root.find(selector));
  }
  function resetTag3FilterState(root) {
    const $root = asFilterRoot(root);
    // 恢复折叠状态
    withSelf($root, '[data-xdex-filter-target="1"]').each((_, el) => {
      $(el)
        .show()
        .removeData('xdex-collapsed')
        .removeAttr('data-xdex-filter-target')
        .removeClass('xdex-generic-collapsed');
    });
    // 恢复隐藏状态
    withSelf($root, '[data-xdex-filter-hidden="1"]').each((_, el) => {
      $(el).show().removeAttr('data-xdex-filter-hidden');
    });
    withSelf($root, '[data-xdex-filter-placeholder="1"]').remove();
  }
  function decorateFilterPlaceholder($ph, $el, placeholderClass, expandedFlex = '0 0 auto') {
    $ph.attr('data-xdex-filter-placeholder', '1').addClass(placeholderClass);
    $el.attr('data-xdex-filter-target', '1');
    if ($ph && $el.hasClass('h-threads-item-reply-main')) {
      const $row  = $el.closest('.h-threads-item-reply');
      const $icon = $row.find('.h-threads-item-reply-icon').first();
      if ($row.length) {
        $row.css({ display: 'flex', alignItems: 'flex-start' });
      }
      if ($icon.length) {
        $icon.css({ flex: '0 0 3em', textAlign: 'center' });
      }
      const applyWidth = () => {
        const txt = ($ph.text() || '').trim();
        if (txt === '点击折叠') {
          $ph.css({
            flex: '0 0 auto',
            maxWidth: '8em',
            whiteSpace: 'nowrap',
            overflow: 'hidden',
            textOverflow: 'ellipsis',
            marginRight: '0.5em'
          });
        } else {
          $ph.css({
            flex: expandedFlex,
            maxWidth: 'none',
            whiteSpace: 'normal',
            overflow: 'visible',
            textOverflow: 'clip',
            marginRight: '0'
          });
        }
      };
      applyWidth();
      $ph.off('click.xdex-filter-width').on('click.xdex-filter-width', () => {
        setTimeout(applyWidth, 0);
      });
    }
  }
  function getThreadIdForElement($el) {
    const $index = $el.closest('.h-threads-item-index[data-threads-id]');
    const indexTid = ($index.attr('data-threads-id') || '').trim();
    if (isValidThreadId(indexTid)) return indexTid;
    const pathTid = PageType.getThreadId(false);
    if (pathTid) return pathTid.slice(0, 8);
    const href = $el.closest('.h-threads-item-index, .h-threads-item, .h-threads-item-reply, .h-threads-item-reply-main')
      .find('.h-threads-info-id[href*="/t/"]').first().attr('href') || '';
    const hrefMatch = href.match(/\/t\/(\d{6,8})/);
    return hrefMatch ? hrefMatch[1].slice(0, 8) : '';
  }
  function addFilterId(ids, value) {
    // 抓取串号/回复号:支持 7/8/9 位(跳过系统 9999999/99999999)
    const matches = String(value || '').match(/\d{7,9}/g) || [];
    matches.forEach((id) => {
      if (id === '9999999' || id === '99999999') return;
      if (!ids.includes(id)) ids.push(id);
    });
  }
  function getFilterIdsForElement($el) {
    const ids = [];
    addFilterId(ids, $el.attr('data-threads-id'));
    addFilterId(ids, $el.closest('[data-threads-id]').attr('data-threads-id'));
    addFilterId(ids, $el.find('.h-threads-info-id').first().text());
    addFilterId(ids, $el.closest('.h-threads-item-index, .h-threads-item, .h-threads-item-reply, .h-threads-item-reply-main').find('.h-threads-info-id').first().text());
    const href = $el.closest('.h-threads-item-index, .h-threads-item, .h-threads-item-reply, .h-threads-item-reply-main').find('.h-threads-info-id[href]').first().attr('href') || '';
    addFilterId(ids, href);
    return ids;
  }
  // ---- 临时解除屏蔽(不修改设置)----
  // 触发方式:链接带 ?xdex_unblock=1,或本会话内 sessionStorage 有该串的豁免标记。
  // 只豁免「串号等于当前页串号」的关键词(正文子串 + ID 匹配两条路径),
  // 其它关键词与正则组不豁免;只看饼干白名单逻辑不受影响。
  // sessionStorage 标记由屏蔽占位条上的「临时显示」按钮写入(见 decorateFilterPlaceholder 附近),
  // 关闭浏览器自动失效。
  function getUnblockExemptThreadId() {
    let parsed = null;
    try { parsed = parseThreadHistoryUrl(location.href); } catch (e) { parsed = null; }
    if (!parsed) return null;
    let viaUrl = false;
    try { viaUrl = new URL(location.href).searchParams.has('xdex_unblock'); } catch (e) { viaUrl = false; }
    let viaSession = false;
    try { viaSession = !!(sessionStorage.getItem('xdex_unblock:' + parsed.threadId)); } catch (e) { viaSession = false; }
    return (viaUrl || viaSession) ? parsed.threadId : null;
  }
  // 判断某串号是否被「串号关键词」屏蔽(临时显示提示条的显示条件)
  function isThreadIdBlockedByKeyword(threadId, blockedKeywords) {
    if (!threadId) return false;
    const normalized = normalizeBlockedKeywordGroups(blockedKeywords);
    return flattenBlockedKeywords(normalized).some((keyword) => (
      isThreadIdKeyword(keyword) && String(keyword || '').trim() === String(threadId)
    ));
  }
  // 屏蔽提示条:仅当当前页串号被串号关键词屏蔽且未豁免时显示。
  // 「临时显示」写 sessionStorage 后刷新(本会话内生效,关浏览器失效);
  // 「×」仅收起本次提示(sessionStorage 记忆,不改变屏蔽行为)。
  function maybeShowUnblockBanner(threadId, cfg) {
    if (!threadId) return;
    if (getUnblockExemptThreadId()) return;
    let dismissed = false;
    try { dismissed = sessionStorage.getItem('xdex_unblock_dismissed:' + threadId) === '1'; } catch (e) { dismissed = false; }
    if (dismissed) return;
    if (!isThreadIdBlockedByKeyword(threadId, (cfg && cfg.blockedKeywords) || [])) return;
    if (document.getElementById('xdex-unblock-banner')) return;
    const bar = document.createElement('div');
    bar.id = 'xdex-unblock-banner';
    bar.style.cssText = 'position:fixed;left:50%;transform:translateX(-50%);bottom:64px;z-index:99998;'
      + 'background:#333;color:#ddd;padding:8px 14px;border-radius:6px;font-size:14px;'
      + 'box-shadow:0 2px 8px rgba(0,0,0,.4);display:flex;gap:10px;align-items:center;';
    const label = document.createElement('span');
    label.textContent = '本串已被关键词屏蔽';
    const showBtn = document.createElement('button');
    showBtn.textContent = '临时显示';
    showBtn.style.cssText = 'background:#00FFCC;color:#00332b;border:0;border-radius:4px;padding:3px 10px;cursor:pointer;font-size:14px;font-weight:600;';
    showBtn.onclick = () => {
      try { sessionStorage.setItem('xdex_unblock:' + threadId, '1'); } catch (e) {}
      location.reload();
    };
    const closeBtn = document.createElement('button');
    closeBtn.textContent = '×';
    closeBtn.title = '本次不显示(屏蔽仍生效)';
    closeBtn.style.cssText = 'background:transparent;color:#888;border:0;cursor:pointer;font-size:14px;line-height:1;padding:0 2px;';
    closeBtn.onclick = () => {
      try { sessionStorage.setItem('xdex_unblock_dismissed:' + threadId, '1'); } catch (e) {}
      bar.remove();
    };
    bar.appendChild(label);
    bar.appendChild(showBtn);
    bar.appendChild(closeBtn);
    (document.body || document.documentElement).appendChild(bar);
  }
  function findBlockedKeywordHit(text, groups, $el) {
    const normalizedGroups = normalizeBlockedKeywordGroups(groups);
    const body = String(text || '');
    // 1) 正则组:整段 pattern 直接匹配正文(不走逗号切分,不做 7/8/9 位 ID 特判)
    for (const group of normalizedGroups) {
      if (!group.regex) continue;
      const re = compileBlockedKeywordRegex(group.value);
      if (!re) continue;
      if (re.test(body)) return group.value;
    }
    // 2) 普通组:逗号切分后正文子串 + 7/8/9 位数字 ID 匹配
    // 临时豁免:当前页串号被「串号关键词」屏蔽且用户通过 ?xdex_unblock=1 / 会话标记打开时,
    // 该关键词从列表剔除(正文子串与 ID 两条路径同时失效),其余关键词照常生效
    const exemptThreadId = getUnblockExemptThreadId();
    const keywords = flattenBlockedKeywords(normalizedGroups)
      .filter((keyword) => !(exemptThreadId && isThreadIdKeyword(keyword) && String(keyword || '').trim() === exemptThreadId));
    const textHit = Utils.firstHit(body, keywords);
    if (textHit) return textHit;
    const ids = getFilterIdsForElement($el);
    return keywords.find((keyword) => isThreadIdKeyword(keyword) && ids.includes(String(keyword || '').trim())) || null;
  }
  function getThreadWhitelistGroup(threadId, groups) {
    if (!threadId || !groups.length) return null;
    for (const group of groups) {
      if ((group.threads || []).includes(threadId)) {
        return group;
      }
    }
    return null;
  }
  function isSystemReplyElement($el) {
    if ($el.closest('.h-threads-item-reply[data-threads-id="9999999"]').length) return true;
    const replyIdText = ($el.find('.h-threads-info-id').first().text() || '').trim();
    return replyIdText === 'No.9999999';
  }
  function isThreadPageForPOAnnotation() {
    return /\/t\/\d{6,8}/.test(location.pathname);
  }
  function isLiveDocumentRoot(root) {
    if (!root || root === document) return true;
    try {
      return !!(root.ownerDocument === document && document.documentElement && document.documentElement.contains(root));
    } catch (e) {
      return false;
    }
  }
  function refreshFilterDisplay(cfg, root) {
    return startupPerfDebug.measure('refreshFilterDisplay', () => {
      if (!isLiveDocumentRoot(root)) {
        applyFilters(cfg, root || document);
        return;
      }
      if (typeof refreshPOAnnotationMode === 'function') {
        refreshPOAnnotationMode(root || document, cfg);
      } else if (typeof applyFilters === 'function') {
        applyFilters(cfg, root || document);
      }
    }, () => startupPerfDebug.summarizeRoot(root || document));
  }
  function getThreadCookieWhitelistDisplayMode(cfg) {
    return (cfg && cfg.threadCookieWhitelistDisplayMode) || 'fold';
  }
  function getPOAnnotationSideDisplayMode(cfg) {
    return (cfg && cfg.poAnnotationSideDisplayMode) || 'collapse';
  }
  function isPOAnnotationActive(cfg) {
    return getThreadCookieWhitelistDisplayMode(cfg) === 'column' && isThreadPageForPOAnnotation();
  }
  function getCookieIdFromReplyMain($reply) {
    return ($reply.find('.h-threads-info-uid').first().text().split(':')[1] || '').trim();
  }
  function detectRuntimePOGroups(root, cfg) {
    const baseCfg = getFilterConfig(cfg);
    if (!isPOAnnotationActive(baseCfg)) return null;
    const threadId = getThreadIdForElement(asFilterRoot(root || document));
    if (!threadId) return null;
    const cookies = [];
    withSelf(asFilterRoot(root || document), '.h-threads-item-reply-main').each((_, el) => {
      const $reply = $(el);
      if (!$reply.find('span.uk-text-primary.uk-text-small').length) return;
      const cid = getCookieIdFromReplyMain($reply);
      if (Utils.cookieLegal(cid) && !cookies.includes(cid)) cookies.push(cid);
    });
    return cookies.length ? { desc: 'PO', threads: [String(threadId).slice(0, 8)], cookies, isAuto: true } : null;
  }
  function buildEffectiveWhitelistGroups(manualGroups, autoPOGroup) {
    const groups = normalizeThreadCookieWhitelistGroups(manualGroups || []).map(g => ({
      desc: g.desc || '',
      threads: [...(g.threads || [])],
      cookies: [...(g.cookies || [])]
    }));
    if (autoPOGroup && autoPOGroup.threads?.length && autoPOGroup.cookies?.length) {
      groups.push({
        desc: autoPOGroup.desc || 'PO',
        threads: [...autoPOGroup.threads],
        cookies: [...autoPOGroup.cookies]
      });
    }
    return mergeThreadCookieWhitelistGroups(groups).groups;
  }
  function buildPOAnnotationLiveConfig(cfg, root) {
    const liveCfg = getFilterConfig(cfg);
    if (!isPOAnnotationActive(liveCfg)) return liveCfg;
    const autoPOGroup = detectRuntimePOGroups(root, liveCfg);
    liveCfg.threadCookieWhitelistGroups = buildEffectiveWhitelistGroups(liveCfg.threadCookieWhitelistGroups || [], autoPOGroup);
    return liveCfg;
  }
  function applyFilters(cfg) {
    const root = arguments.length > 1 ? arguments[1] : undefined;
    return startupPerfDebug.measure('applyFilters', () => {
    const $root = asFilterRoot(root);
    cfg = getFilterConfig(cfg);
    if (applyFilters._running) return; // 防重入
    applyFilters._running = true;
    try {
    resetTag3FilterState($root);
    // 标记
    markAllCookies(cfg.markedGroups||[], $root);
    // 屏蔽(按组,匹配到则折叠,文案含备注)
    const blkG = (cfg.blockedCookies||[]);
    const blkKGroups = normalizeBlockedKeywordGroups(cfg.blockedKeywords);
    const whitelistGroups = normalizeThreadCookieWhitelistGroups(cfg.threadCookieWhitelistGroups || []);
    const whitelistDisplayMode = getThreadCookieWhitelistDisplayMode(cfg);
    const poAnnotationMode = isPOAnnotationActive(cfg);
    const displayMode = cfg.blockDisplayMode || 'fold';
    const checkBlocked = $el => {
      if ($el.closest('.h-preview-box').length) return;
      if (isSystemReplyElement($el)) return;
      const cid = ($el.find('.h-threads-info-uid').first().text().split(':')[1]||'').trim();
      const txt = $el.find('.h-threads-content').first().text();
      if (cid && blkG.length) {
        let matchedCookie = null, matchedDesc = '';
        for (let i=0; i<blkG.length && !matchedCookie; i++){
          const g = blkG[i];
          const hit = g.cookies.find(p=>Utils.cookieMatch(cid,p));
          if (hit) { matchedCookie = hit; matchedDesc = g.desc || ''; }
        }
        if (matchedCookie) {
          if (displayMode === 'hide') {
            const _c = $el.closest('.h-threads-item-reply'); const $target = _c.length ? _c : $el;
            // 板块页主串:隐藏下方的 <hr> 分隔线
            if ($target.hasClass('h-threads-item-index')) {
              const $hr = $target.next('hr');
              if ($hr.length) $hr.hide().attr('data-xdex-filter-hidden', '1');
            }
            $target.hide().attr('data-xdex-filter-hidden', '1');
          } else {
            const label = matchedDesc ? `${matchedCookie}:${matchedDesc}` : matchedCookie;
            const $ph = Utils.collapse($el, `饼干屏蔽『${label}』`);
            if ($ph) decorateFilterPlaceholder($ph, $el, 'xdex-placeholder-blocked', '0 0 auto');
          }
          return;
        }
      }
      const kw = findBlockedKeywordHit(txt, blkKGroups, $el);
      if (kw) {
        if (displayMode === 'hide') {
          const _c = $el.closest('.h-threads-item-reply'); const $target = _c.length ? _c : $el;
            // 板块页主串:隐藏下方的 <hr> 分隔线
            if ($target.hasClass('h-threads-item-index')) {
              const $hr = $target.next('hr');
              if ($hr.length) $hr.hide().attr('data-xdex-filter-hidden', '1');
            }
          $target.hide().attr('data-xdex-filter-hidden', '1');
        } else {
          const $ph = Utils.collapse($el, `关键词屏蔽『${kw}』`);
          if ($ph) decorateFilterPlaceholder($ph, $el, 'xdex-placeholder-blocked', '0 0 auto');
        }
      }
    };
    const checkWhitelistReply = ($reply, whitelistGroup) => {
      if ($reply.closest('.h-preview-box').length) return;
      if (isSystemReplyElement($reply)) return;
      const cid = ($reply.find('.h-threads-info-uid').first().text().split(':')[1] || '').trim();
      if (!cid) return;
      const whitelistCookies = whitelistGroup?.cookies || [];
      if (whitelistCookies.some((pattern) => Utils.cookieMatch(cid, pattern))) {
        return;
      }
      if (poAnnotationMode) return;
      if (whitelistDisplayMode === 'hide') {
        const _c = $reply.closest('.h-threads-item-reply');
        const $target = _c.length ? _c : $reply;
        $target.hide().attr('data-xdex-filter-hidden', '1');
        return;
      }
      // 只看饼干始终保持折叠,不受 displayMode 影响
      const prefix = whitelistGroup && whitelistGroup.desc ? `『${whitelistGroup.desc}』` : '';
      const $ph = Utils.collapse($reply, `${prefix}只看饼干『${whitelistCookies.join(',')}』`);
      if ($ph) decorateFilterPlaceholder($ph, $reply, 'xdex-placeholder-whitelist', '0 0 auto');
    };
    if(/\/t\/\d{6,8}/.test(location.pathname)){
      const currentThreadId = getThreadIdForElement($root);
      const whitelistGroup = getThreadWhitelistGroup(currentThreadId, whitelistGroups);
      withSelf($root, '.h-threads-item-reply-main').each((_,el)=>{
        const $reply = $(el);
        if (whitelistGroup) {
          checkWhitelistReply($reply, whitelistGroup);
        } else {
          checkBlocked($reply);
        }
      });
    } else {
      const $threads = withSelf($root, '.h-threads-item-index');
      if ($threads.length) {
        $threads.each((_,el)=>{
        const $th=$(el);
        const threadId = getThreadIdForElement($th);
        const whitelistGroup = getThreadWhitelistGroup(threadId, whitelistGroups);
        if (!whitelistGroup) {
          checkBlocked($th);
        }
        $th.find('.h-threads-item-reply-main').each((_,s)=>{
          const $reply = $(s);
          if (whitelistGroup) {
            checkWhitelistReply($reply, whitelistGroup);
          } else {
            checkBlocked($reply);
          }
        });
        });
      } else {
        withSelf($root, '.h-threads-item-reply-main').each((_, el) => {
          const $reply = $(el);
          const threadId = getThreadIdForElement($reply);
          const whitelistGroup = getThreadWhitelistGroup(threadId, whitelistGroups);
          if (whitelistGroup) {
            checkWhitelistReply($reply, whitelistGroup);
          } else {
            checkBlocked($reply);
          }
        });
      }
    }
    // 串号关键词屏蔽当前页时给「临时显示」入口(不改设置;详见 getUnblockExemptThreadId 注释)
    try {
      const currentThread = parseThreadHistoryUrl(location.href);
      if (currentThread && !getUnblockExemptThreadId()) maybeShowUnblockBanner(currentThread.threadId, cfg);
    } catch (e) {}
    } finally { applyFilters._running = false; }
    }, () => startupPerfDebug.summarizeRoot(root || document));
  }
  function resetPOAnnotationLayout(root) {
    const scope = root || document;
    const anchors = Array.from(scope.querySelectorAll ? scope.querySelectorAll('[data-xdex-po-annotation-anchor="1"]') : []);
    anchors.forEach(anchor => {
      const key = anchor.getAttribute('data-xdex-po-annotation-key');
      const repliesRoot = anchor.closest('.h-threads-item-replies') || document;
      const moved = key ? repliesRoot.querySelector(`.h-threads-item-reply[data-xdex-po-annotation-moved="${key}"]`) : null;
      if (moved && anchor.parentNode) {
        anchor.parentNode.insertBefore(moved, anchor);
        moved.removeAttribute('data-xdex-po-annotation-moved');
        moved.removeAttribute('data-xdex-po-annotation-role');
      }
      anchor.remove();
    });
    const wrappers = Array.from(scope.querySelectorAll ? scope.querySelectorAll('[data-xdex-po-annotation-wrapper="1"]') : []);
    wrappers.forEach(wrapper => {
      const repliesRoot = wrapper.closest('.h-threads-item-replies');
      const mainReplies = Array.from(wrapper.querySelectorAll('.xdex-po-annotation-main > .h-threads-item-reply'));
      mainReplies.forEach(reply => {
        if (repliesRoot) {
          reply.removeAttribute('data-xdex-po-annotation-highlight');
          reply.removeAttribute('data-xdex-po-annotation-role');
          repliesRoot.insertBefore(reply, wrapper);
        }
      });
      const sideReplies = Array.from(wrapper.querySelectorAll('.h-threads-item-reply[data-xdex-po-annotation-moved]'));
      sideReplies.forEach(reply => {
        if (repliesRoot) {
          reply.removeAttribute('data-xdex-po-annotation-moved');
          reply.removeAttribute('data-xdex-po-annotation-role');
          repliesRoot.insertBefore(reply, wrapper);
        }
      });
      wrapper.remove();
    });
    Array.from(scope.querySelectorAll ? scope.querySelectorAll('.h-threads-item-reply[data-xdex-po-annotation-moved]') : []).forEach(reply => {
      const repliesRoot = reply.closest('.h-threads-item-replies');
      if (repliesRoot && reply.parentNode !== repliesRoot) {
        const fallbackWrapper = repliesRoot.querySelector('[data-xdex-po-annotation-wrapper="1"]');
        if (fallbackWrapper && fallbackWrapper.parentNode === repliesRoot) {
          repliesRoot.insertBefore(reply, fallbackWrapper);
        } else {
          repliesRoot.appendChild(reply);
        }
      }
      reply.removeAttribute('data-xdex-po-annotation-moved');
      reply.removeAttribute('data-xdex-po-annotation-role');
    });
    Array.from(scope.querySelectorAll ? scope.querySelectorAll('[data-xdex-po-annotation-anchor="1"], [data-xdex-po-annotation-wrapper="1"]') : []).forEach(el => el.remove());
    Array.from(scope.querySelectorAll ? scope.querySelectorAll('[data-xdex-po-annotation-audience], [data-xdex-po-annotation-highlight]') : []).forEach(el => {
      el.removeAttribute('data-xdex-po-annotation-audience');
      el.removeAttribute('data-xdex-po-annotation-highlight');
      el.removeAttribute('data-xdex-po-annotation-role');
    });
  }
  function ensurePOAnnotationStyle() {
    if (document.getElementById('xdex-po-annotation-style')) return;
    const style = document.createElement('style');
    style.id = 'xdex-po-annotation-style';
    style.textContent = `
      .xdex-po-annotation-group{display:grid;grid-template-columns:minmax(0,1fr) clamp(220px,31vw,380px);gap:10px;align-items:start;margin:6px 0;overflow:visible;}
      .xdex-po-annotation-main{min-width:0;position:relative;z-index:2;overflow:visible;}
      .xdex-po-annotation-side{min-width:0;border-left:2px solid #d2b59e;padding-left:8px;opacity:.92;position:relative;z-index:1;overflow:visible;}
      .xdex-po-annotation-side-title{font-size:12px;color:#886b58;margin:0 0 4px;cursor:pointer;user-select:none;}
      .xdex-po-annotation-side-body{min-width:0;overflow:visible;}
      .xdex-po-annotation-side[data-side-display-mode="collapse"] .xdex-po-annotation-side-body{overflow:auto;}
      .xdex-po-annotation-side .h-threads-item-reply{font-size:12px;margin-bottom:6px;}
      .xdex-po-annotation-main .h-threads-item-reply{position:relative;z-index:2;overflow:visible;}
      .xdex-po-annotation-main .h-threads-img-box.h-active{position:relative;z-index:20;overflow:visible !important;}
      .xdex-po-annotation-main .h-threads-img-box.h-active .h-threads-img-a{position:relative;z-index:21;overflow:visible !important;}
      .xdex-po-annotation-main .h-threads-img-box.h-active .h-threads-img{position:relative;z-index:22;}
      .xdex-po-annotation-side .h-threads-item-reply-main,.xdex-po-annotation-side .h-threads-content,.xdex-po-annotation-side .h-threads-img-box,.xdex-po-annotation-side .h-threads-img-a,.xdex-po-annotation-side .h-threads-img,.xdex-po-annotation-side img{max-width:100% !important;height:auto !important;box-sizing:border-box;}
      .xdex-po-annotation-side .h-threads-item-reply-main{width:100% !important;}
      .xdex-po-annotation-side .h-threads-img-box{width:100% !important;max-width:100% !important;}
      .xdex-po-annotation-side .h-threads-img-a{display:block;max-width:100% !important;overflow:hidden;}
      .xdex-po-annotation-side .h-threads-img{max-width:100% !important;height:auto !important;}
      .xdex-po-annotation-side .h-threads-img-box.h-active{max-width:100% !important;overflow:hidden !important;}
      .xdex-po-annotation-side .h-threads-img-box .h-threads-img-tool-btn{font-size:11px;white-space:nowrap;}
      @media (max-width: 860px){.xdex-po-annotation-group{display:block}.xdex-po-annotation-side{border-left:0;border-top:1px dashed #d2b59e;margin-top:6px;padding:6px 0 0;}}
    `;
    document.head.appendChild(style);
  }
  function openPOAnnotationReplyQuote(replyEl) {
    if (!replyEl) return false;
    const tid = (replyEl.getAttribute('data-threads-id') || '').trim();
    if (!/^\d{1,}$/.test(tid) || tid === '9999999') return false;
    try {
      if (typeof window.__xdexOpenQuoteByTid !== 'function' && typeof enableQuotePreview === 'function') {
        enableQuotePreview();
      }
      if (typeof window.__xdexOpenQuoteByTid !== 'function') return false;
      const ret = window.__xdexOpenQuoteByTid(tid, {
        fromPOImage: true,
        currentThreadId: (window.__xdexQuoteRefMark && window.__xdexQuoteRefMark.getContextThreadId(replyEl)) || ''
      });
      if (ret && typeof ret.then === 'function') {
        ret.catch(() => {});
      }
      return true;
    } catch (e) {
      return false;
    }
  }
  function bindPOAnnotationSideImageQuotePreview() {
    if (document.documentElement.dataset.xdexPoImageQuoteBound === '1') return;
    document.documentElement.dataset.xdexPoImageQuoteBound = '1';
    document.addEventListener('click', (e) => {
      if (e.defaultPrevented) return;
      if (typeof e.button === 'number' && e.button !== 0) return;
      if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
      const trigger = e.target.closest('.xdex-po-annotation-side .h-threads-img-a');
      if (!trigger) return;
      const replyEl = trigger.closest('.h-threads-item-reply');
      if (!replyEl) return;
      const opened = openPOAnnotationReplyQuote(replyEl);
      if (!opened) return;
      e.preventDefault();
      e.stopPropagation();
      if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
    }, true);
  }
  function updatePOAnnotationSideState(group, expanded) {
    if (!group) return;
    const side = group.querySelector('.xdex-po-annotation-side');
    const title = group.querySelector('.xdex-po-annotation-side-title');
    const body = group.querySelector('.xdex-po-annotation-side-body');
    const main = group.querySelector('.xdex-po-annotation-main');
    if (!side || !title || !body || !main) return;
    side.setAttribute('data-side-expanded', expanded ? '1' : '0');
    const displayMode = expanded ? 'expand' : 'collapse';
    side.setAttribute('data-side-display-mode', displayMode);
    title.setAttribute('data-side-expanded', expanded ? '1' : '0');
    if (expanded) {
      body.style.maxHeight = 'none';
      body.style.overflowY = 'visible';
    } else {
      const mainHeight = Math.max(main.offsetHeight || 0, 88);
      body.style.maxHeight = `${mainHeight}px`;
      body.style.overflowY = body.scrollHeight > mainHeight ? 'auto' : 'visible';
    }
  }
  function bindPOAnnotationSideToggle(group, defaultMode) {
    if (!group || group.getAttribute('data-xdex-po-side-toggle-bound') === '1') return;
    group.setAttribute('data-xdex-po-side-toggle-bound', '1');
    const title = group.querySelector('.xdex-po-annotation-side-title');
    if (!title) return;
    title.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      const side = group.querySelector('.xdex-po-annotation-side');
      const expanded = !(side && side.getAttribute('data-side-expanded') === '1');
      updatePOAnnotationSideState(group, expanded);
    });
    updatePOAnnotationSideState(group, defaultMode === 'expand');
  }
  function isPOAnnotationHighlightReply(reply, whitelistGroup) {
    if (!reply || !whitelistGroup) return false;
    if (reply.getAttribute('data-threads-id') === '9999999') return false;
    const main = reply.querySelector('.h-threads-item-reply-main');
    if (!main) return false;
    const cid = ($(main).find('.h-threads-info-uid').first().text().split(':')[1] || '').trim();
    return !!cid && (whitelistGroup.cookies || []).some(pattern => Utils.cookieMatch(cid, pattern));
  }
  function applyPOAnnotationLayout(root, liveCfg) {
    if (!isPOAnnotationActive(liveCfg)) return;
    if (window.innerWidth && window.innerWidth < 860) return;
    const rootNode = root || document;
    const threadId = getThreadIdForElement(asFilterRoot(rootNode));
    const whitelistGroup = getThreadWhitelistGroup(threadId, normalizeThreadCookieWhitelistGroups(liveCfg.threadCookieWhitelistGroups || []));
    if (!whitelistGroup) return;
    ensurePOAnnotationStyle();
    bindPOAnnotationSideImageQuotePreview();
    const sideDisplayMode = getPOAnnotationSideDisplayMode(liveCfg);
    const containers = [];
    if (rootNode.matches && rootNode.matches('.h-threads-item-replies')) containers.push(rootNode);
    if (rootNode.querySelectorAll) rootNode.querySelectorAll('.h-threads-item-replies').forEach(el => containers.push(el));
    const uniqueContainers = [...new Set(containers)];
    uniqueContainers.forEach(repliesRoot => {
      const replies = Array.from(repliesRoot.children).filter(el => el.classList && el.classList.contains('h-threads-item-reply'));
      const highlights = replies.filter(reply => isPOAnnotationHighlightReply(reply, whitelistGroup));
      if (highlights.length < 2) return;
      let currentGroup = null;
      let movedCount = 0;
      let continuationGroup = null;
      const createGroup = (reply, sideTitle, continuationText = '') => {
        const group = document.createElement('div');
        group.className = 'xdex-po-annotation-group';
        group.setAttribute('data-xdex-po-annotation-wrapper', '1');
        const main = document.createElement('div');
        main.className = 'xdex-po-annotation-main';
        const side = document.createElement('div');
        side.className = 'xdex-po-annotation-side';
        side.setAttribute('data-xdex-po-annotation-side', '1');
        side.setAttribute('data-side-display-mode', sideDisplayMode);
        const title = document.createElement('div');
        title.className = 'xdex-po-annotation-side-title';
        title.textContent = sideTitle;
        side.appendChild(title);
        const sideBody = document.createElement('div');
        sideBody.className = 'xdex-po-annotation-side-body';
        side.appendChild(sideBody);
        if (continuationText) {
          const continuation = document.createElement('div');
          continuation.className = 'xdex-po-annotation-side-title';
          continuation.textContent = continuationText;
          main.appendChild(continuation);
        }
        repliesRoot.insertBefore(group, reply);
        group.appendChild(main);
        group.appendChild(side);
        bindPOAnnotationSideToggle(group, sideDisplayMode);
        return { wrapper: group, main, side };
      };
      replies.forEach(reply => {
        if (isPOAnnotationHighlightReply(reply, whitelistGroup)) {
          reply.setAttribute('data-xdex-po-annotation-highlight', '1');
          const group = createGroup(reply, '批注');
          group.main.appendChild(reply);
          currentGroup = group;
          return;
        }
        if (reply.getAttribute('data-threads-id') === '9999999') return;
        if (reply.querySelector('.h-threads-item-reply-main[data-xdex-filter-hidden="1"]')) return;
        if (!currentGroup) {
          continuationGroup = continuationGroup || createGroup(reply, '批注(续)', '上页续');
        }
        const targetGroup = currentGroup || continuationGroup;
        if (!targetGroup) return;
        const key = `po-${Date.now()}-${Math.random().toString(36).slice(2)}`;
        const anchor = document.createElement('span');
        anchor.setAttribute('data-xdex-po-annotation-anchor', '1');
        anchor.setAttribute('data-xdex-po-annotation-key', key);
        anchor.style.display = 'none';
        repliesRoot.insertBefore(anchor, reply);
        reply.setAttribute('data-xdex-po-annotation-moved', key);
        reply.setAttribute('data-xdex-po-annotation-role', 'side');
        const sideBody = targetGroup.side.querySelector('.xdex-po-annotation-side-body') || targetGroup.side;
        sideBody.appendChild(reply);
        movedCount++;
        updatePOAnnotationSideState(targetGroup.wrapper, sideDisplayMode === 'expand');
      });
      Array.from(repliesRoot.querySelectorAll('[data-xdex-po-annotation-wrapper="1"]')).forEach(wrapper => {
        const hasSideReply = !!wrapper.querySelector('.xdex-po-annotation-side .h-threads-item-reply');
        if (!hasSideReply) {
          const mainReplies = Array.from(wrapper.querySelectorAll('.xdex-po-annotation-main > .h-threads-item-reply'));
          mainReplies.forEach(reply => repliesRoot.insertBefore(reply, wrapper));
          wrapper.remove();
        } else {
          updatePOAnnotationSideState(wrapper, sideDisplayMode === 'expand');
        }
      });
    });
  }
  function refreshPOAnnotationMode(root, cfg) {
    const targetRoot = root || document;
    resetPOAnnotationLayout(targetRoot);
    const liveCfg = buildPOAnnotationLiveConfig(cfg, targetRoot);
    applyFilters(liveCfg, targetRoot);
    try {
      applyPOAnnotationLayout(targetRoot, liveCfg);
    } catch (e) {
      try { resetPOAnnotationLayout(targetRoot); } catch (_) {}
      console.warn('PO annotation layout failed, fallback to filters-only', e);
    }
  }

  /* --------------------------------------------------
   * tag 4. 外部图床显示
   * -------------------------------------------------- */
  const ExternalImagePreview = (function(){
    let started = false;
    const PROCESSED_ATTRIBUTE = 'data-images-processed';
    const DEFAULT_VISIBLE = 3;
    const LOAD_BATCH = 3;
    const imageUrlRegex = /(https?:\/\/[^\s'")\]}]+?\.(?:jpg|jpeg|png|gif|bmp|webp|svg)(?:\?[^\s'")\]}]*)?)(?=$|\s|['")\]}.,!?])/gi;
    function buttonCss() {
      return `
        font-size: 12px;
        padding: 4px 10px;
        margin-left: 6px;
        color: #333;
        background: #fff;
        border: 1px solid #ccc;
        border-radius: 4px;
        cursor: pointer;
      `;
    }
    function createImageItem(url, containerWidth) {
      const frag = document.createDocumentFragment();
      const img = document.createElement('img');
      img.src = url;
      img.style.cssText = `
        display: block;
        margin: 8px auto 2px auto;
        border: 1px solid #ccc;
        border-radius: 3px;
        cursor: pointer;
        height: auto;
      `;
      const linkDiv = document.createElement('div');
      linkDiv.style.cssText = `
        font-size: 12px;
        color: #666;
        margin: 0 auto 10px auto;
        word-break: break-all;
        width: fit-content;
        max-width: 100%;
      `;
      const a = document.createElement('a');
      a.href = url;
      a.target = '_blank';
      a.textContent = url;
      a.style.color = '#007bff';
      a.addEventListener('click', (e) => e.stopPropagation());
      linkDiv.appendChild(a);
      img.addEventListener('load', () => {
        const naturalW = img.naturalWidth || 0;
        if (naturalW > containerWidth) {
          img.style.width = Math.round(containerWidth * 0.75) + 'px';
          img.dataset.state = 'large';
        } else {
          img.style.width = naturalW + 'px';
          img.dataset.state = 'small';
        }
      });
      img.addEventListener('error', () => {
        img.style.display = 'none';
        linkDiv.style.display = 'none';
      });
      img.addEventListener('click', (e) => {
        e.stopPropagation();
        const state = img.dataset.state;
        if (state === 'large') {
          window.open(url, '_blank');
        } else if (state === 'small') {
          const naturalW = img.naturalWidth || 0;
          const targetW = Math.round(containerWidth * 0.75);
          if (!img.dataset.enlarged && targetW > naturalW) {
            img.style.width = targetW + 'px';
            img.dataset.enlarged = 'true';
          } else {
            window.open(url, '_blank');
          }
        }
      });
      frag.appendChild(img);
      frag.appendChild(linkDiv);
      return frag;
    }
    function appendImages(bodyEl, urls, containerWidth) {
      const frag = document.createDocumentFragment();
      urls.forEach((url) => frag.appendChild(createImageItem(url, containerWidth)));
      bodyEl.appendChild(frag);
    }
    function setCollapsed(container, collapsed) {
      container.classList.toggle('collapsed', collapsed);
      container.dataset.collapsed = collapsed ? 'true' : 'false';
      container.querySelectorAll('.iic-toggle-btn').forEach((btn) => {
        btn.textContent = collapsed ? '展开' : '收起';
      });
    }
    function injectContainer(afterDiv, imageUrls) {
      const total = imageUrls.length;
      if (total === 0) return;
      const container = document.createElement('div');
      container.className = 'injected-image-container';
      container.style.cssText = `
        margin: 12px 0;
        border: 1px solid #ddd;
        border-radius: 6px;
        background-color: #f9f9f9;
        box-shadow: 0 1px 2px rgba(0,0,0,0.03);
        overflow: hidden;
      `;
      container.dataset.total = String(total);
      container.dataset.collapsed = 'false';
      const header = document.createElement('div');
      header.className = 'iic-header';
      header.style.cssText = `
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 10px 14px;
        background: #f2f2f2;
        border-bottom: 1px solid #e6e6e6;
        cursor: default;
        user-select: none;
      `;
      const title = document.createElement('span');
      title.className = 'iic-title';
      title.textContent = `图片预览(${total})`;
      title.style.cssText = `font-size: 13px; color: #333;`;
      const actions = document.createElement('div');
      actions.className = 'iic-actions';
      const moreTopBtn = document.createElement('button');
      moreTopBtn.className = 'iic-more-btn-top';
      moreTopBtn.style.cssText = buttonCss();
      const toggleBtn = document.createElement('button');
      toggleBtn.className = 'iic-toggle-btn';
      toggleBtn.textContent = '收起';
      toggleBtn.style.cssText = buttonCss();
      toggleBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        const next = container.dataset.collapsed !== 'true';
        setCollapsed(container, next);
      });
      actions.appendChild(moreTopBtn);
      actions.appendChild(toggleBtn);
      header.appendChild(title);
      header.appendChild(actions);
      const body = document.createElement('div');
      body.className = 'iic-body';
      body.style.cssText = `
        padding: 12px 24px 10px 24px;
        overflow-x: auto;
      `;
      const footer = document.createElement('div');
      footer.className = 'iic-footer';
      footer.style.cssText = `
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 8px 12px 12px 12px;
        background: #f9f9f9;
        border-top: 1px solid #eee;
      `;
      const moreBottomBtn = document.createElement('button');
      moreBottomBtn.className = 'iic-more-btn-bottom';
      moreBottomBtn.style.cssText = buttonCss();
      footer.appendChild(moreBottomBtn);
      const style = document.createElement('style');
      style.textContent = `
        .injected-image-container.collapsed .iic-body,
        .injected-image-container.collapsed .iic-footer { display: none; }
      `;
      container.appendChild(style);
      container.appendChild(header);
      container.appendChild(body);
      container.appendChild(footer);
      afterDiv.parentNode.insertBefore(container, afterDiv.nextSibling);
      const containerWidth = body.clientWidth || 600;
      const visibleUrls = imageUrls.slice(0, DEFAULT_VISIBLE);
      const queue = imageUrls.slice(DEFAULT_VISIBLE);
      appendImages(body, visibleUrls, containerWidth);
      function remainingCount() { return queue.length; }
      function updateMoreButtons() {
        const rem = remainingCount();
        const label = rem > 0 ? `展开更多(剩余${rem},+${LOAD_BATCH})` : '已全部展开';
        moreTopBtn.textContent = label;
        moreBottomBtn.textContent = label;
        const display = rem > 0 ? '' : 'none';
        moreTopBtn.style.display = display;
        moreBottomBtn.style.display = display;
      }
      function loadMore(e) {
        e.stopPropagation();
        if (queue.length === 0) return;
        const batch = queue.splice(0, LOAD_BATCH);
        appendImages(body, batch, containerWidth);
        updateMoreButtons();
      }
      moreTopBtn.addEventListener('click', loadMore);
      moreBottomBtn.addEventListener('click', loadMore);
      updateMoreButtons();
      container.addEventListener('click', (e) => {
        const target = e.target;
        if (target.closest('img, button, a, input, label, textarea, select')) return;
        if (container.dataset.collapsed !== 'true') setCollapsed(container, true);
      });
    }
    function processDiv(div) {
      if (div.hasAttribute(PROCESSED_ATTRIBUTE)) return;
      // 如果是预览框且已经有图片,则直接标记为已处理并跳过
      if (div.classList.contains('h-preview-box') && div.querySelector('img')) {
        div.setAttribute(PROCESSED_ATTRIBUTE, 'true');
        return;
      }
      div.setAttribute(PROCESSED_ATTRIBUTE, 'true');
      const textContent = div.textContent || div.innerText || '';
      const matches = textContent.match(imageUrlRegex);
      if (!matches || matches.length === 0) return;
      const uniqueImageUrls = [...new Set(matches.map((u) => u.trim()))];
      if (uniqueImageUrls.length === 0) return;
      injectContainer(div, uniqueImageUrls);
    }
    function findAndProcessDivs() {
      const divs = document.querySelectorAll(
        'div.h-threads-content:not([' + PROCESSED_ATTRIBUTE + ']), ' +
        'div.h-preview-box:not([' + PROCESSED_ATTRIBUTE + '])'
      );
      divs.forEach(processDiv);
    }
    function observeChanges() {
      const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (mutation.type === 'childList') {
            mutation.addedNodes.forEach((node) => {
              if (node.nodeType !== 1) return;
              if (node.classList && node.classList.contains('injected-image-container')) return;
              if (
                node.classList &&
                (node.classList.contains('h-threads-content') || node.classList.contains('h-preview-box')) &&
                !node.hasAttribute(PROCESSED_ATTRIBUTE)
              ) {
                processDiv(node);
              }
              const childDivs = node.querySelectorAll && node.querySelectorAll(
                'div.h-threads-content:not([' + PROCESSED_ATTRIBUTE + ']), ' +
                'div.h-preview-box:not([' + PROCESSED_ATTRIBUTE + '])'
              );
              if (childDivs && childDivs.length > 0) {
                childDivs.forEach(processDiv);
              }
            });
          }
        });
      });
      observer.observe(document.body, { childList: true, subtree: true });
    }
    function init() {
      if (started) return;
      started = true;
      setTimeout(findAndProcessDivs, 100);
      observeChanges();
      window.addEventListener('load', () => setTimeout(findAndProcessDivs, 500));
      // 调试辅助
      window.resetImageScript = function resetProcessedElements() {
        document.querySelectorAll('[' + PROCESSED_ATTRIBUTE + ']').forEach((el) => el.removeAttribute(PROCESSED_ATTRIBUTE));
        document.querySelectorAll('.injected-image-container').forEach((c) => c.remove());
      };
    }
    return { init };
  })();

  /* --------------------------------------------------
   * tag 5. 手动切换饼干 + 自动刷新饼干
   * -------------------------------------------------- */
  const abbreviateName = n => n.replace(/\s*-\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}$/, '');
  const getCookiesList   = () => GM_getValue('cookies', {});
  const getCurrentCookie = () => GM_getValue('now-cookie', null);
  const LAST_USED_COOKIE_KEY = 'xdex-last-used-cookie';
  const getLastUsedCookie = () => GM_getValue(LAST_USED_COOKIE_KEY, null);
  const isCookieListUnavailable = (list) => !list || Object.keys(list).length === 0;
  const getLikelyActiveCookie = () => getCurrentCookie() || getLastUsedCookie();
  const COOKIE_USERHASH_DIGEST_CACHE_KEY = 'xdex-cookie-userhash-digests';
  const cookieDigestInflight = new Map();
  let cookieListUnavailableState = false;
  const LOGIN_PROMPT_SUPPRESS_KEY = 'loginPromptSuppressAuto';
  const getLoginPromptSuppressAuto = () => !!GM_getValue(LOGIN_PROMPT_SUPPRESS_KEY, false);
  const setLoginPromptSuppressAuto = (v) => GM_setValue(LOGIN_PROMPT_SUPPRESS_KEY, !!v);

  function getCookieUserhashDigestCache() {
    const cache = GM_getValue(COOKIE_USERHASH_DIGEST_CACHE_KEY, {});
    return cache && typeof cache === 'object' ? cache : {};
  }
  function setCookieUserhashDigestCache(cache) {
    GM_setValue(COOKIE_USERHASH_DIGEST_CACHE_KEY, cache && typeof cache === 'object' ? cache : {});
  }
  function getCurrentBrowserUserhash() {
    const match = String(document.cookie || '').match(/(?:^|;\s*)userhash=([^;]+)/);
    if (!match) return null;
    try { return decodeURIComponent(match[1]); } catch (e) { return match[1]; }
  }
  async function digestUserhash(raw) {
    if (!raw || !globalThis.crypto || !globalThis.crypto.subtle || typeof TextEncoder === 'undefined') return null;
    try {
      const data = new TextEncoder().encode(raw);
      const hash = await globalThis.crypto.subtle.digest('SHA-256', data);
      return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
    } catch (e) {
      return null;
    }
  }
  function getQueryParamPreservePlus(search, name) {
    const query = String(search || '').replace(/^\?/, '');
    const pairs = query.split('&');
    for (const pair of pairs) {
      const idx = pair.indexOf('=');
      const key = idx >= 0 ? pair.slice(0, idx) : pair;
      let decodedKey;
      try { decodedKey = decodeURIComponent(key); } catch (e) { decodedKey = key; }
      if (decodedKey !== name) continue;
      const value = idx >= 0 ? pair.slice(idx + 1) : '';
      try { return decodeURIComponent(value); } catch (e) { return value; }
    }
    return null;
  }
  function decodeCookieExportText(text) {
    if (!text || typeof atob !== 'function' || typeof TextDecoder === 'undefined') return null;
    try {
      const bin = atob(text);
      const bytes = new Uint8Array(bin.length);
      for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
      const jsonText = new TextDecoder('utf-8').decode(bytes);
      try { return JSON.parse(jsonText); }
      catch (e) { return JSON.parse(jsonText.replace(/\+/g, '%20')); }
    } catch (e) {
      return null;
    }
  }
  function parseCookieExportUserhash(html) {
    const doc = new DOMParser().parseFromString(String(html || ''), 'text/html');
    const node = doc.querySelector('img[src*="text="]') || doc.querySelector('[src*="text="]');
    const src = node && node.getAttribute('src');
    if (!src) return null;
    try {
      const url = new URL(src, location.origin);
      const text = getQueryParamPreservePlus(url.search, 'text') || url.searchParams.get('text');
      const data = decodeCookieExportText(text);
      return data && typeof data.cookie === 'string' ? data.cookie : null;
    } catch (e) {
      return null;
    }
  }
  function getCookieDigestCacheEntry(cookie) {
    return {
      id: cookie.id,
      name: cookie.name,
      desc: cookie.desc,
      updatedAt: Date.now()
    };
  }
  async function fetchCookieExportUserhashDigest(cookie) {
    if (!cookie || !cookie.id) return null;
    const id = String(cookie.id);
    if (cookieDigestInflight.has(id)) return cookieDigestInflight.get(id);
    const promise = (async () => {
      try {
        const resp = await gmRequest(`https://www.nmbxd1.com/Member/User/Cookie/export/id/${encodeURIComponent(id)}.html`, 'text');
        const raw = parseCookieExportUserhash(resp.responseText || '');
        const digest = await digestUserhash(raw);
        return digest ? Object.assign(getCookieDigestCacheEntry(cookie), { digest }) : null;
      } catch (e) {
        return null;
      }
    })();
    cookieDigestInflight.set(id, promise);
    try { return await promise; }
    finally { cookieDigestInflight.delete(id); }
  }
  async function syncCookieUserhashDigestCache(list) {
    const ids = Object.keys(list || {});
    if (!ids.length) return;
    const cache = getCookieUserhashDigestCache();
    const next = {};
    const missing = [];
    ids.forEach(id => {
      const cookie = list[id];
      if (!cookie) return;
      if (cache[id] && cache[id].digest) {
        next[id] = Object.assign({}, cache[id], getCookieDigestCacheEntry(cookie));
      } else {
        missing.push(cookie);
      }
    });
    setCookieUserhashDigestCache(next);
    if (!missing.length) return;
    const results = await Promise.allSettled(missing.map(fetchCookieExportUserhashDigest));
    results.forEach(result => {
      if (result.status === 'fulfilled' && result.value && result.value.id && result.value.digest) {
        next[String(result.value.id)] = result.value;
      }
    });
    setCookieUserhashDigestCache(next);
  }
  async function getCookieMatchedByCurrentUserhash() {
    const raw = getCurrentBrowserUserhash();
    const digest = await digestUserhash(raw);
    if (!digest) return null;
    const cache = getCookieUserhashDigestCache();
    const entry = Object.values(cache).find(item => item && item.digest === digest);
    return entry ? { id: entry.id, name: entry.name, desc: entry.desc } : null;
  }
  async function getLikelyActiveCookieAsync() {
    const matched = await getCookieMatchedByCurrentUserhash();
    return matched || getLikelyActiveCookie();
  }
  function removeDateString(){
    $('#cookie-switcher-ui').find('*').addBack().contents()
      .filter(function(){ return this.nodeType===3; })
      .each(function(){
        this.nodeValue = this.nodeValue.replace(/ - 0000-00-00 00:00:00/g,'');
      });
  }
  function updateCurrentCookieDisplay(cur){
    // 当前饼干变化 → 同步串内偏好开关圆圈着色(置于 $d 守卫之前,各入口均可刷新)
    try { if (typeof refreshCookiePrefSwitchState === 'function') refreshCookiePrefSwitchState(); } catch (e) {}
    const $d = $('#current-cookie-display');
    if(!$d.length) return;
    if(cookieListUnavailableState){
      $d.text('待确认').css('color','red');
    } else if(cur){
      const nm = abbreviateName(cur.name);
      $d.text(nm + (cur.desc ? ' - ' + cur.desc : '')).css('color','#000');
    } else {
      $d.text('无饼干').css('color','red');
      //showLoginPrompt(); // 这里触发
    }
    removeDateString();
  // 饼干变化 → 冷却提示跟随切换(各饼干独立计时,切回仍显示剩余时间)
  try { renderCookieCooldown(); } catch (e) {}
  }

  // ---- 饼干发送冷却(服务端 15s 限制)----
  // 只有「服务器明确回执成功」才记录计时起点:失败提交不消耗冷却,因此不进入冷却态。
  // 切换饼干后按各自的起点独立显示;15s 走完提示自动消失。
  const COOKIE_SUBMIT_COOLDOWN_MS = 15000;
  const COOKIE_COOLDOWN_STORE_KEY = 'xdex-cookie-cooldowns';
  const COOKIE_COOLDOWN_TICK_MS = 500;
  let cookieCooldownTimer = 0;
  function getCookieCooldownStore() {
    const store = GM_getValue(COOKIE_COOLDOWN_STORE_KEY, {});
    return store && typeof store === 'object' ? store : {};
  }
  function setCookieCooldownAt(cookieId, atMs) {
    const id = String(cookieId || '');
    if (!id) return;
    const store = getCookieCooldownStore();
    if (Number(atMs) > 0) store[id] = Number(atMs);
    else delete store[id];
    // 顺手清理已过期的键,避免记录无限增长
    const now = Date.now();
    Object.keys(store).forEach((k) => {
      if (!(now - Number(store[k]) < COOKIE_SUBMIT_COOLDOWN_MS)) delete store[k];
    });
    GM_setValue(COOKIE_COOLDOWN_STORE_KEY, store);
  }
  function getCookieCooldownRemainMs(cookie) {
    const cur = cookie || getCurrentCookie();
    if (!cur || !cur.id) return 0;
    const at = Number(getCookieCooldownStore()[String(cur.id)] || 0);
    if (!at) return 0;
    const remain = at + COOKIE_SUBMIT_COOLDOWN_MS - Date.now();
    return remain > 0 ? remain : 0;
  }
  // 提交成功:从此刻开始计时
  function startCookieCooldown(cookieId) {
    const id = String(cookieId || (getCurrentCookie() || {}).id || '');
    if (!id) return;
    setCookieCooldownAt(id, Date.now());
    renderCookieCooldown();
  }
  // 服务端回执里带「冷却时间[N]s」→ 用它校准剩余(仅在本饼干已有成功计时记录时生效,
  // 避免一次失败提交凭空造出冷却;这仍是「以服务器回复为准」的校准)
  function syncCookieCooldownFromServer(msg) {
    const m = String(msg == null ? '' : msg).match(/冷却时间\s*[[[]\s*(\d+)\s*[\]]]/);
    if (!m) return false;
    const cur = getCurrentCookie();
    if (!cur || !cur.id) return false;
    const store = getCookieCooldownStore();
    if (!store[String(cur.id)]) return false;
    const remainMs = Math.max(0, Number(m[1]) || 0) * 1000;
    setCookieCooldownAt(cur.id, Date.now() + remainMs - COOKIE_SUBMIT_COOLDOWN_MS);
    renderCookieCooldown();
    return true;
  }
  // 跨页面同步:其它标签页发送成功后,本标签页(回复框已打开的)也要实时显示/更新冷却。
  // 数据本身在 GM 存储共享;这里补的是「变更通知 → 重渲染」。
  // 性能与生命周期:
  //   · 全页面只注册一次(cookieCooldownSyncBound 守卫),绝不随饼干切换/面板刷新重复注册
  //   · 监听器随页面上下文销毁自动失效,无需手动注销
  //   · 事件只在该键被写入时投递(负载为整个 store,15s 冷却下每饼干每 15s 至多 1 次,
  //     实际大小 = 饼干数 × 数字,几十字节),回调仅做一次幂等的 renderCookieCooldown
  //   · 本页自己的写入也会触发本监听(remote=false)——renderCookieCooldown 幂等,重复调用无害;
  //     且 renderCookieCooldown 内部有 cookieCooldownTimer 守卫,走秒 interval 不会重复叠加
  let cookieCooldownSyncBound = false;
  function bindCookieCooldownStorageSync() {
    if (cookieCooldownSyncBound || typeof GM_addValueChangeListener !== 'function') return;
    cookieCooldownSyncBound = true;
    GM_addValueChangeListener(COOKIE_COOLDOWN_STORE_KEY, () => {
      try { renderCookieCooldown(); } catch (e) {}
    });
  }
  function getCookieCooldownEl() {
    return document.querySelector('.xdex-cookie-cooldown');
  }
  // 模块初始化:一次性注册跨标签同步监听(幂等,见上)
  bindCookieCooldownStorageSync();
  // 冷却提示固定在「字数」左侧,二者同属送出按钮容器(右对齐)
  function ensureCookieCooldownEl() {
    const counter = document.querySelector('.xdex-post-content-char-count');
    if (!counter || !counter.parentNode) return null;
    let el = counter.parentNode.querySelector('.xdex-cookie-cooldown');
    if (!el) {
      el = document.createElement('span');
      el.className = 'xdex-cookie-cooldown';
      // margin-right:与「字数」之间留出间距(容器本身有 gap:6px,这里再加一点更清晰)
      el.style.cssText = 'font-size:12px;color:#888;white-space:nowrap;flex:0 0 auto;display:none;margin-right:4px;';
      counter.parentNode.insertBefore(el, counter);
    }
    return el;
  }
  function stopCookieCooldownTicker() {
    if (!cookieCooldownTimer) return;
    clearInterval(cookieCooldownTimer);
    cookieCooldownTimer = 0;
  }
  function renderCookieCooldown() {
    const el = ensureCookieCooldownEl();
    if (!el) return;
    const remain = getCookieCooldownRemainMs();
    if (remain <= 0) {
      el.style.display = 'none';
      el.textContent = '';
      stopCookieCooldownTicker();
      return;
    }
    const seconds = Math.max(1, Math.ceil(remain / 1000));
    el.style.display = '';
    // 默认区域(非拓展弹窗)空间窄,「冷却时间」前缀会与颜文字按钮重叠 → 只显示 [N]s;
    // 拓展弹窗(.qp-body 内)空间充足 → 显示完整「冷却时间[N]s」
    const inPopup = !!(el.closest && el.closest('.qp-body'));
    // 数字盒定宽 2ch + 内部「右对齐」:
    //   · 个位数(8)贴住右侧 ] ,两位数(15)自然填满 → 数字本身不跳
    //   · 盒子宽度不变 → 左侧「冷却时间[」与右侧「]s」「字数」都不移动
    // vertical-align:-0.08em:15px 数字在基线上比 12px 灰字高约 1px(实测 delta -1px),
    //   下压后两者字形中心对齐(残差 0.19px),视觉上居中而非偏上
    el.innerHTML = (inPopup ? '冷却时间' : '')
      + '[<span class="xdex-cookie-cooldown-num" '
      + 'style="color:#e53935;font-size:15px;font-weight:700;line-height:1;'
      + 'display:inline-block;min-width:2ch;text-align:right;vertical-align:-0.08em;'
      + 'font-variant-numeric:tabular-nums;font-feature-settings:\'tnum\' 1;">'
      + seconds + '</span>]s';
    if (!cookieCooldownTimer) {
      // 仅在存在冷却时走秒;冷却结束立即停表,不留常驻定时器
      cookieCooldownTimer = setInterval(() => {
        const box = getCookieCooldownEl();
        if (!box) { stopCookieCooldownTicker(); return; }
        if (getCookieCooldownRemainMs() <= 0) {
          box.style.display = 'none';
          box.textContent = '';
          stopCookieCooldownTicker();
          return;
        }
        renderCookieCooldown();
      }, COOKIE_COOLDOWN_TICK_MS);
    }
  }
  function updateDropdownUI(list){
    const $dd = $('#cookie-dropdown'); $dd.empty();
    const ids = Object.keys(list);
    ids.forEach(id=>{
      const c=list[id];
      const txt=abbreviateName(c.name)+(c.desc?' - '+c.desc:'');
      $dd.append(`<option value="${id}">${txt}</option>`);
    });
    const cur = getCurrentCookie();
    if (cur && list[cur.id]) {
      $dd.val(cur.id);
    } else {
      $dd.val(ids[0] || '');
    }
    removeDateString();
  }
  function scheduleCookieDropdownFocusRestore(dropdown, delay = 120) {
    if (!dropdown) return;
    const target = dropdown.__focusBackTarget;
    if (!target) return;
    if (dropdown.__focusBackTimer) {
      clearTimeout(dropdown.__focusBackTimer);
    }
    dropdown.__focusBackTimer = setTimeout(() => {
      dropdown.__focusBackTimer = null;
      if (document.activeElement === dropdown && target.isConnected) {
        try { target.focus(); } catch (e) {}
      }
      delete dropdown.__focusBackTarget;
      delete dropdown.__openedValue;
    }, delay);
  }
  function closeCookieShortcutMenu() {
    const existing = document.getElementById('xdex-cookie-shortcut-menu');
    if (existing) existing.remove();
  }
  function openCookieShortcutMenu(dropdown, focusBackTarget) {
    closeCookieShortcutMenu();
    const options = Array.from(dropdown.options || []);
    if (!options.length) return false;
    const selectedValue = dropdown.value;
    const normalStyle = 'display:block;width:100%;box-sizing:border-box;border:0;background:transparent;color:inherit;text-align:left;padding:6px 12px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;outline:none;';
    const selectedStyle = normalStyle + 'background:#eaf7ff;outline:2px solid #66ccff;outline-offset:-2px;box-shadow:inset 3px 0 0 #66ccff;';
    const hoverStyle = normalStyle + 'background:#f2f2f2;';
    const selectedHoverStyle = normalStyle + 'background:#dff3ff;outline:2px solid #66ccff;outline-offset:-2px;box-shadow:inset 3px 0 0 #66ccff;';
    const activeStyle = normalStyle + 'background:#e8e8e8;outline:2px solid #66ccff;outline-offset:-2px;';
    const activeSelectedStyle = normalStyle + 'background:#dff3ff;outline:2px solid #66ccff;outline-offset:-2px;box-shadow:inset 3px 0 0 #66ccff;';
    const menu = document.createElement('div');
    menu.id = 'xdex-cookie-shortcut-menu';
    menu.tabIndex = -1;
    menu.style.cssText = 'position:fixed;z-index:2147483647;min-width:220px;max-width:420px;max-height:50vh;overflow:auto;background:#fff;color:#222;border:1px solid rgba(0,0,0,.25);border-radius:6px;box-shadow:0 4px 18px rgba(0,0,0,.25);font-size:14px;line-height:1.4;padding:4px 0;';
    function applyCookieShortcutItemStyle(item) {
      if (!item) return;
      const isCurrent = item.dataset.current === '1';
      const isActive = item.dataset.active === '1';
      item.style.cssText = isActive ? (isCurrent ? activeSelectedStyle : activeStyle) : (isCurrent ? selectedStyle : normalStyle);
    }
    function setActiveCookieShortcutItem(nextItem) {
      menu.querySelectorAll('button[data-value]').forEach((item) => {
        item.dataset.active = item === nextItem ? '1' : '0';
        applyCookieShortcutItemStyle(item);
      });
      if (nextItem) {
        nextItem.focus();
        nextItem.scrollIntoView({ block: 'nearest' });
      }
    }
    options.forEach((option) => {
      const isSelected = option.value === selectedValue;
      const item = document.createElement('button');
      item.type = 'button';
      item.textContent = option.textContent || option.value;
      item.dataset.value = option.value;
      item.dataset.current = isSelected ? '1' : '0';
      item.dataset.active = '0';
      if (isSelected) item.setAttribute('aria-current', 'true');
      applyCookieShortcutItemStyle(item);
      item.addEventListener('mouseenter', () => { item.style.cssText = isSelected ? selectedHoverStyle : hoverStyle; });
      item.addEventListener('mouseleave', () => { applyCookieShortcutItemStyle(item); });
      item.addEventListener('click', () => {
        dropdown.__focusBackTarget = focusBackTarget || dropdown.__focusBackTarget || document.activeElement || null;
        dropdown.value = item.dataset.value || '';
        dropdown.dispatchEvent(new Event('change', { bubbles: true }));
        // 先移除菜单再交还焦点:菜单若仍在文档中,焦点会落到即将被移除的按钮上
        closeCookieShortcutMenu();
        if (typeof restorePostTextareaFocus === 'function') restorePostTextareaFocus();
      });
      menu.appendChild(item);
    });
    const rect = dropdown.getBoundingClientRect();
    document.documentElement.appendChild(menu);
    const width = Math.max(rect.width || 220, 220);
    menu.style.width = `${width}px`;
    const menuRect = menu.getBoundingClientRect();
    const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - menuRect.width - 8));
    const top = Math.min(Math.max(8, rect.bottom + 4), Math.max(8, window.innerHeight - menuRect.height - 8));
    menu.style.left = `${left}px`;
    menu.style.top = `${top}px`;
    menu.addEventListener('keydown', (e) => {
      const items = Array.from(menu.querySelectorAll('button[data-value]'));
      const active = items.find((item) => item.dataset.active === '1') || document.activeElement;
      const index = Math.max(0, items.indexOf(active));
      if (e.key === 'Escape') {
        e.preventDefault();
        closeCookieShortcutMenu();
        if (focusBackTarget && focusBackTarget.isConnected) focusBackTarget.focus();
      } else if (e.key === 'ArrowDown') {
        e.preventDefault();
        setActiveCookieShortcutItem(items[Math.min(items.length - 1, index + 1)]);
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        setActiveCookieShortcutItem(items[Math.max(0, index - 1)]);
      } else if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        active && active.click && active.click();
      }
    });
    document.addEventListener('mousedown', function onMouseDown(e) {
      if (menu.contains(e.target)) return;
      closeCookieShortcutMenu();
      document.removeEventListener('mousedown', onMouseDown, true);
    }, true);
    const selected = Array.from(menu.querySelectorAll('button[data-value]')).find((item) => item.dataset.value === dropdown.value) || menu.querySelector('button[data-value]');
    if (selected) setActiveCookieShortcutItem(selected);
    logCookieDropdownShortcutStage('custom-menu-opened', { options: options.length, value: dropdown.value });
    return true;
  }
  // 把焦点交回发帖输入框(多处共用:切换饼干成功后、快捷菜单选择瞬间、弹窗取消等)
  function restorePostTextareaFocus() {
    const textarea = document.querySelector('textarea.h-post-form-textarea');
    if (textarea && typeof textarea.focus === 'function') {
      try { textarea.focus(); } catch (e) {}
    }
  }
  function switch_cookie(cookie, opts = {}){
    const silent = !!opts.silent;
    const onDone = typeof opts.onDone === 'function' ? opts.onDone : null;
    const onFail = typeof opts.onFail === 'function' ? opts.onFail : null;
    if(!cookie || !cookie.id) {
      if (!silent) toast('无效的饼干信息!', 0, { type: 'error' });
      onFail && onFail();
      return;
    }
    // ★ 选择即回归焦点:不等网络往返,避免换饼期间(网络延迟时)阻塞用户输入
    if (opts.focusBackNow) restorePostTextareaFocus();
    $.get(`https://www.nmbxd1.com/Member/User/Cookie/switchTo/id/${cookie.id}.html`)
      .done(()=>{
        if (!silent) toast('切换成功! 当前饼干为 '+abbreviateName(cookie.name));
        GM_setValue('now-cookie',cookie);
        GM_setValue(LAST_USED_COOKIE_KEY,cookie);
        updateCurrentCookieDisplay(cookie);
        updateDropdownUI(getCookiesList());
        removeDateString();
        updatePreviewCookieId();
        // 切换成功后,将焦点移回到 textarea(兜底:即时恢复可能被后续 UI 更新打断)
        setTimeout(() => { restorePostTextareaFocus(); }, 100);
        onDone && onDone(cookie);
      })
      .fail(()=>{
        if (!silent) toast('切换失败,请重试', 0, { type: 'error' });
        onFail && onFail();
      });
  }
  function autoApplyFirstCookieIfNeeded(list, opts = {}) {
      const ids = Object.keys(list || {});
      if (!ids.length) return false;
      const cur = getCurrentCookie();
      if (cur && list[cur.id]) return false;
      const lastUsed = getLastUsedCookie();
      const preferred = lastUsed && list[lastUsed.id] ? list[lastUsed.id] : null;
      const first = list[ids[0]];
      const target = preferred || first;
      if (!target || !target.id) return false;
      if (window.__cookieAutoApplying) return true;
      window.__cookieAutoApplying = true;
      switch_cookie(target, {
        silent: !!opts.silent,
        onDone: (cookie) => {
          window.__cookieAutoApplying = false;
          if (opts.showDefaultToast) {
            toast(preferred ? '已应用最近饼干:' + abbreviateName(cookie.name) : '已应用默认饼干:' + abbreviateName(cookie.name));
          }
          if (typeof opts.onDone === 'function') opts.onDone(cookie);
        },
        onFail: () => {
          window.__cookieAutoApplying = false;
          if (typeof opts.onFail === 'function') opts.onFail();
        }
      });
      return true;
    }
  function refreshCookies(cb, showToast = true, opts = {}){
    GM_xmlhttpRequest({
      method:'GET',
      url:'https://www.nmbxd1.com/Member/User/Cookie/index.html',
      onload: async r=>{
        if(r.status!==200){ toast('刷新失败 HTTP '+r.status, 0, { type: 'error' }); return cb&&cb(); }
        const doc=new DOMParser().parseFromString(r.responseText,'text/html');
        const rows=doc.querySelectorAll('tbody>tr'), list={};
        rows.forEach(row=>{
          const tds=row.querySelectorAll('td');
          if(tds.length>=4){
            const id=tds[1].textContent.trim();
            const name=(tds[2].querySelector('a')||{}).textContent?.trim?.() || (tds[2].textContent||'').trim();
            const desc=tds[3].textContent.trim();
            list[id]={id,name,desc};
          }
        });
        GM_setValue('cookies',list);
        cookieListUnavailableState = isCookieListUnavailable(list);
        let actualCookieBeforeSync = null;
        if (!cookieListUnavailableState) {
          try { actualCookieBeforeSync = await getCookieMatchedByCurrentUserhash(); } catch (e) {}
          try { await syncCookieUserhashDigestCache(list); } catch (e) {}
        }
        updateDropdownUI(list);
        if (showToast) {
          toast('饼干列表已刷新!');
        }
        let cur=getCurrentCookie();
        let deletedActualCookie = null;
        if (cookieListUnavailableState) {
          cur = await getLikelyActiveCookieAsync();
          if (!cur) cur = null;
        } else if (actualCookieBeforeSync && actualCookieBeforeSync.id) {
          if (list[actualCookieBeforeSync.id]) {
            cur = list[actualCookieBeforeSync.id];
          } else {
            deletedActualCookie = actualCookieBeforeSync;
            cur = null;
            GM_setValue('now-cookie', null);
          }
        } else if(cur && !list[cur.id]) cur=null;
        // 若已登录且有饼干列表,但当前未应用任何饼干,则优先自动应用最近使用的饼干,否则应用第一个
        if (!cur && Object.keys(list).length) {
          const started = autoApplyFirstCookieIfNeeded(list, {
            silent: true,
            showDefaultToast: !deletedActualCookie,
            onDone: (cookie) => {
              if (deletedActualCookie && cookie && cookie.name) {
                toast('当前饼干 ' + abbreviateName(deletedActualCookie.name) + ' 已被删除,已切换到 ' + abbreviateName(cookie.name));
              }
              cb&&cb();
            },
            onFail: () => {
              GM_setValue('now-cookie', null);
              updateCurrentCookieDisplay(null);
              removeDateString();
              updatePreviewCookieId();
              cb&&cb();
            }
          });
          if (started) return;
        }
        GM_setValue('now-cookie',cur);
        updateCurrentCookieDisplay(cur);
        removeDateString();
        updatePreviewCookieId();
        // === 新增:检测是否无饼干,触发登录提示 ===
        const $display = $('#current-cookie-display');
        const $dropdown = $('#cookie-dropdown');
        if (
          !$dropdown.children().length ||
          ($display.length && $display.text().trim() === '已删除')
        ) {
          showLoginPrompt(!!opts.manualPrompt);
        }
        cb&&cb();
      },
      onerror:()=>{
        toast('刷新失败,网络错误', 0, { type: 'error' }); cb&&cb();
      }
    });
  }
  function ensureLoginPromptStyle() {
    if (document.getElementById('xdex-login-prompt-style')) return;
    const style = document.createElement('style');
    style.id = 'xdex-login-prompt-style';
    style.textContent = `
      :root {
        --xdex-login-backdrop-bg: rgba(0,0,0,.45);
        --xdex-login-dialog-bg: #FFFFEE;
        --xdex-login-dialog-border: #d6c7ba;
        --xdex-login-dialog-text: #2f241d;
        --xdex-login-dialog-muted: #6a5546;
        --xdex-login-dialog-shadow: rgba(0,0,0,.28);
        --xdex-login-btn-bg: #f0e0d6;
        --xdex-login-btn-hover-bg: #e7d3c7;
        --xdex-login-btn-border: #c7b5a8;
        --xdex-login-btn-text: #4d392c;
        --xdex-login-btn-primary-bg: #66CCFF;
        --xdex-login-btn-primary-hover-bg: #54c2f7;
        --xdex-login-btn-primary-border: #59b9e7;
        --xdex-login-btn-primary-text: #fff;
      }
      :root.xdex-darkreader-active {
        --xdex-login-backdrop-bg: rgba(8,8,9,.62);
        --xdex-login-dialog-bg: #2F3233;
        --xdex-login-dialog-border: #3a2a21;
        --xdex-login-dialog-text: #f2dfc7;
        --xdex-login-dialog-muted: #d2bda4;
        --xdex-login-dialog-shadow: rgba(0,0,0,.6);
        --xdex-login-btn-bg: #3a2a21;
        --xdex-login-btn-hover-bg: #4a3429;
        --xdex-login-btn-border: #5d4334;
        --xdex-login-btn-text: #f2dfc7;
        --xdex-login-btn-primary-bg: #8fb3ff;
        --xdex-login-btn-primary-hover-bg: #a4c0ff;
        --xdex-login-btn-primary-border: #7da4f6;
        --xdex-login-btn-primary-text: #1a2232;
      }
      #login-modal-wrapper {
        position: fixed;
        inset: 0;
        z-index: 10000;
        display: flex;
        align-items: flex-start;
        justify-content: center;
      }
      #login-modal-wrapper .login-backdrop {
        position: absolute;
        inset: 0;
        background: var(--xdex-login-backdrop-bg);
      }
      #login-modal-wrapper .login-dialog {
        position: relative;
        top: 30%;
        width: min(400px, calc(100vw - 32px));
        padding: 20px;
        background: var(--xdex-login-dialog-bg);
        border: 1px solid var(--xdex-login-dialog-border);
        border-radius: 8px;
        box-shadow: 0 8px 24px var(--xdex-login-dialog-shadow);
        color: var(--xdex-login-dialog-text);
        z-index: 10001;
        box-sizing: border-box;
      }
      #login-modal-wrapper.xdex-login-embedded {
        align-items: center;
      }
      #login-modal-wrapper.xdex-login-embedded .login-dialog {
        top: 0;
        width: min(920px, calc(100vw - 24px));
        height: min(88vh, 860px);
        padding: 12px;
        display: flex;
        flex-direction: column;
        gap: 8px;
      }
      #login-modal-wrapper.xdex-login-embedded .login-dialog.xdex-login-dialog-pending {
        visibility: hidden;
      }
      #login-modal-wrapper.xdex-login-embedded .login-dialog h2,
      #login-modal-wrapper.xdex-login-embedded .login-dialog p {
        flex: 0 0 auto;
      }
      #login-modal-wrapper.xdex-login-embedded .login-dialog-frame-wrap {
        flex: 1 1 auto;
        min-height: 0;
        border: 1px solid var(--xdex-login-dialog-border);
        border-radius: 6px;
        overflow: hidden;
        background: #fff;
      }
      #login-modal-wrapper.xdex-login-embedded .login-dialog-frame {
        display: block;
        width: 100%;
        height: 100%;
        border: 0;
        background: #fff;
      }
      #login-modal-wrapper.xdex-login-embedded .login-dialog-frame.xdex-login-frame-pending {
        visibility: hidden;
      }
      #login-modal-wrapper.xdex-login-embedded .login-dialog-status {
        min-height: 0;
        margin: 0;
        color: var(--xdex-login-dialog-muted);
      }
      #login-modal-wrapper.xdex-login-embedded .login-dialog-actions {
        margin-top: 4px;
      }
      #login-modal-wrapper .login-dialog h2 {
        margin: 0 0 12px;
        color: inherit;
      }
      #login-modal-wrapper .login-dialog p {
        margin: 0 0 8px;
        color: var(--xdex-login-dialog-muted);
      }
      #login-modal-wrapper .login-dialog-actions {
        margin-top: 16px;
        text-align: right;
        display: flex;
        justify-content: flex-end;
        gap: 10px;
        flex-wrap: wrap;
      }
      #login-modal-wrapper .login-dialog-actions button {
        margin-left: 10px;
        padding: 6px 12px;
        background: var(--xdex-login-btn-bg);
        border: 1px solid var(--xdex-login-btn-border);
        border-radius: 6px;
        color: var(--xdex-login-btn-text);
        box-shadow: none;
        cursor: pointer;
      }
      #login-modal-wrapper .login-dialog-actions button:hover,
      #login-modal-wrapper .login-dialog-actions button:focus {
        background: var(--xdex-login-btn-hover-bg);
        outline: none;
      }
      #login-modal-wrapper #login-open {
        background: var(--xdex-login-btn-primary-bg);
        border-color: var(--xdex-login-btn-primary-border);
        color: var(--xdex-login-btn-primary-text);
      }
      #login-modal-wrapper #login-open:hover,
      #login-modal-wrapper #login-open:focus {
        background: var(--xdex-login-btn-primary-hover-bg);
      }
    `;
    document.head.appendChild(style);
  }
  function disableVerifyInputMemory(root = document) {
    if (!root) return;
    const targets = [];
    if (root.nodeType === 1 && root.matches && (root.matches('#doc-ipt-verify-1') || root.matches('input[name="verify"]'))) {
      targets.push(root);
    }
    if (root.querySelectorAll) {
      root.querySelectorAll('#doc-ipt-verify-1, input[name="verify"]').forEach((el) => targets.push(el));
    }
    targets.forEach((el) => {
      if (!el || el.dataset.xdexVerifyMemoryDisabled === '1') return;
      el.setAttribute('autocomplete', 'off');
      el.setAttribute('autocapitalize', 'off');
      el.setAttribute('autocorrect', 'off');
      el.setAttribute('spellcheck', 'false');
      el.dataset.xdexVerifyMemoryDisabled = '1';
    });
  }
  //done : 弹出登录提示弹窗已修复
  function showLoginPrompt(force = false){
    const url = window.location.href;
    const allowed = (
      url.startsWith("https://www.nmbxd1.com/t/") ||
      url.startsWith("https://www.nmbxd1.com/f/") ||
      url.startsWith("https://www.nmbxd1.com/Forum/timeline/")
    );
    if (!allowed) return; // 不在指定页面 → 直接退出
    if (document.body && /429\s*Too Many Requests/i.test(document.body.textContent || '')) return; // 429 限流页面,不弹登录提示
    if (!force && (window.__loginPromptSuppressUntilRefresh || getLoginPromptSuppressAuto())) return; // 持久化“不再提醒”后,自动场景不再弹出
    if (!force && window.__loginPromptShown) return; // 自动触发只弹一次
    if ($('#login-modal-wrapper').length) return; // 避免重复插入
    window.__loginPromptShown = true;
    ensureLoginPromptStyle();
    function updateLoggedOutCookieHint($dialog) {
      const $hint = $dialog.find('.login-cookie-hint');
      if (!$hint.length) return;
      const applyLoginCookieMark = () => {
        if (typeof markAllCookies !== 'function') return;
        try {
          const cfg = getFilterConfig();
          markAllCookies(cfg.markedGroups || [], $dialog[0]);
        } catch (e) {}
      };
      const setHint = (prefix, cookieName, suffix) => {
        $hint.empty();
        if (!cookieName) {
          $hint.text(prefix);
          return;
        }
        $hint.append(document.createTextNode(prefix));
        $hint.append($('<span class="h-threads-info-uid"></span>').text(`ID:${cookieName}`));
        $hint.append(document.createTextNode(suffix));
        applyLoginCookieMark();
      };
      const likely = getLikelyActiveCookie();
      if (likely && likely.name) {
        setHint('缓存中可用饼干可能为:', abbreviateName(likely.name), '。此时仍可作为该饼干回复。');
      } else {
        setHint('当前仍可能按最后一次应用的饼干回复,但暂时无法确认具体饼干。');
      }
      getCookieMatchedByCurrentUserhash().then(matched => {
        if (!matched || !matched.name || !isCookieListUnavailable(getCookiesList())) return;
        setHint('当前实际作用饼干为:', abbreviateName(matched.name), '。此时仍可作为该饼干回复。');
      });
    }
    let pollTimer = null;
    let pollTimeout = null;
    let isChecking = false;
    let embeddedMode = false;
    const cleanup = () => {
      if (pollTimer) {
        clearInterval(pollTimer);
        pollTimer = null;
      }
      if (pollTimeout) {
        clearTimeout(pollTimeout);
        pollTimeout = null;
      }
      isChecking = false;
    };
    const closePrompt = () => {
      cleanup();
      $m.fadeOut(200, () => $m.remove());
    };
    const refreshStatus = (text) => {
      $m.find('.login-dialog-status').text(text || '');
    };
    const checkLoginState = (onDone) => {
      if (isChecking) return;
      isChecking = true;
      refreshCookies(() => {
        isChecking = false;
        const hasCookies = Object.keys(getCookiesList() || {}).length > 0;
        if (hasCookies) {
          cleanup();
          toast('登录成功,饼干已刷新');
          closePrompt();
          return;
        }
        if (typeof onDone === 'function') onDone(false);
      }, false, { manualPrompt: false });
    };
    const startPolling = () => {
      cleanup();
      refreshStatus('Tips:已进入嵌入登录模式,完成后会自动检测。');
      checkLoginState();
      pollTimer = setInterval(() => {
        checkLoginState((ok) => {
          if (!ok) refreshStatus('Tips:登陆未完成');
        });
      }, 2000);
      pollTimeout = setTimeout(() => {
        cleanup();
        refreshStatus('Tips:自动检测已停止,请在完成登录后点击“我已完成登录,刷新饼干”。');
      }, 120000);
    };
    const renderEmbeddedMode = () => {
      if (embeddedMode) return;
      embeddedMode = true;
      $m.addClass('xdex-login-embedded');
      const $dialog = $m.find('.login-dialog');
      $dialog.addClass('xdex-login-dialog-pending');
      $dialog.html(`
        <h2>登录</h2>
        <div class="login-dialog-frame-wrap">
          <iframe class="login-dialog-frame xdex-login-frame-pending" src="https://www.nmbxd1.com/Member/User/Index/login.html" title="登录页面"></iframe>
        </div>
        <p class="login-dialog-status"></p>
        <div class="login-dialog-actions">
          <button id="login-refresh-after-login">我已完成登录,刷新饼干</button>
          <button id="login-close-embedded">关闭</button>
        </div>
      `);
      const compactEmbeddedLoginPage = (doc) => {
        if (!doc || !doc.head) return false;
        if (!/\/Member\/User\/Index\/login\.html(?:$|[?#])/.test(doc.location.href)) return false;
        if (doc.getElementById('xdex-compact-login-style')) return true;
        const style = doc.createElement('style');
        style.id = 'xdex-compact-login-style';
        style.textContent = `
          html, body {
            height: auto !important;
            min-height: 0 !important;
            overflow: auto !important;
          }
          body[data-type="login"] {
            overflow: auto !important;
          }
          .myapp-login {
            height: auto !important;
            min-height: 0 !important;
            padding: 8px 0 !important;
          }
          .myapp-login-logo-block {
            padding: 0 !important;
          }
          .tpl-login-max {
            max-width: 520px !important;
            margin: 0 auto !important;
          }
          .myapp-login-logo-text {
            display: none !important;
          }
          .login-font {
            padding: 4px 0 8px !important;
            font-size: 11px !important;
            line-height: 1.2 !important;
          }
          .login-am-center {
            width: auto !important;
            max-width: 420px !important;
            float: none !important;
          }
          .login-am-center .am-form-group {
            margin-bottom: 8px !important;
          }
          .login-am-center .am-form input {
            height: 34px !important;
            line-height: 26px !important;
            font-size: 12px !important;
          }
          img[src*="/Member/User/Index/verify.html"] {
            margin-top: 4px !important;
          }
          .am-form p {
            margin: 8px 0 0 !important;
          }
          .login-am-center .am-btn-default {
            min-height: 34px !important;
            line-height: 28px !important;
          }
        `;
        doc.head.appendChild(style);
        return true;
      };
      const applyEmbeddedLoginAdjustments = () => {
        const iframe = $m.find('.login-dialog-frame').get(0);
        if (!iframe) return;
        try {
          const doc = iframe.contentDocument;
          if (doc) {
            disableVerifyInputMemory(doc);
            if (compactEmbeddedLoginPage(doc)) {
              iframe.classList.remove('xdex-login-frame-pending');
              $dialog.removeClass('xdex-login-dialog-pending');
            }
          }
        } catch (e) {}
      };
      const $iframe = $m.find('.login-dialog-frame');
      $iframe.on('load', applyEmbeddedLoginAdjustments);
      applyEmbeddedLoginAdjustments();
      $('#login-close-embedded').on('click', () => {
        closePrompt();
      });
      $('#login-refresh-after-login').on('click', () => {
        refreshStatus('Tips:正在检查登录状态...');
        checkLoginState((ok) => {
          if (!ok) refreshStatus('Tips:仍未检测到饼干,请确认已完成登录。');
        });
      });
      $m.off('click').on('click', (e) => {
        if (e.target.classList && e.target.classList.contains('login-backdrop')) {
          closePrompt();
        }
      });
      startPolling();
    };
    const $m = $(`
      <div id="login-modal-wrapper">
        <!-- 遮罩层 -->
        <div class="login-backdrop"></div>
        <!-- 弹窗内容 -->
        <div class="login-dialog">
          <h2>提示</h2>
          <p class="login-cookie-warning">当前已退出登录,无法切换饼干。</p>
          <p class="login-cookie-hint" style="color:red;">当前仍可能按最后一次应用的饼干回复,但暂时无法确认具体饼干。</p>
          <div class="login-dialog-actions">
            <button id="login-open">去登录</button>
            <button id="login-no-remind">不再提醒</button>
            <!-- <button id="login-close">关闭</button> -->
          </div>
        </div>
      </div>
    `);
    $('body').append($m);
    updateLoggedOutCookieHint($m);
    $('#login-open').on('click', () => {
      toast('正在打开登录面板……');
      renderEmbeddedMode();
    });
    $('#login-close').on('click', () => {
      closePrompt();
    });
    $('#login-no-remind').on('click', () => {
      window.__loginPromptSuppressUntilRefresh = true;
      setLoginPromptSuppressAuto(true);
      closePrompt();
    });
    $m.on('click', (e) => {
      if (e.target.classList && e.target.classList.contains('login-backdrop')) {
        closePrompt();
      }
    });
  }
  function createCookieSwitcherUI(){
    const $title = $('.h-post-form-title:contains("回应模式")').first();
    let $grid = $title.closest('.uk-grid.uk-grid-small.h-post-form-grid');
    if(!$grid.length)
      $grid = $('.h-post-form-title:contains("名 称")').first()
        .closest('.uk-grid.uk-grid-small.h-post-form-grid');
    if(!$grid.length) return;
    const cur = getCurrentCookie(), list = getCookiesList();
    cookieListUnavailableState = isCookieListUnavailable(list);
    const $ui = $(`
      <div class="uk-grid uk-grid-small h-post-form-grid" id="cookie-switcher-ui" style="display: flex; flex-wrap: nowrap; align-items: center; width: 100%;">
        <div class="uk-width-1-5">
          <div class="h-post-form-title">饼干</div>
        </div>
        <div class="uk-width-4-5 h-post-form-input" style="display:flex;align-items:center;gap:8px;flex-wrap:nowrap;">
          <div style="flex:1 1 auto;display:flex;align-items:center;gap:6px;min-width:3ch;">
            <span id="current-cookie-display" style="max-width:40%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></span>
            <select id="cookie-dropdown" style="flex:1 1 auto;min-width:3ch;max-width:100%;"></select>
          </div>
          <button id="apply-cookie" class="uk-button uk-button-default" style="display:none;">应用</button>
          <div style="margin-left:auto;flex:0 0 auto;display:flex;align-items:center;">
            <button id="refresh-cookie" class="xdex-icon-btn" title="刷新饼干列表" style="min-width:1em;text-align:center;">${XDEX_ICON_SYNC}</button>
          </div>
        </div>
      </div>`);
    $grid.before($ui);
    updateCurrentCookieDisplay(cur);
    updateDropdownUI(list);
    // 登录但未应用任何饼干时,优先自动应用最近使用的饼干,否则应用第一个
    autoApplyFirstCookieIfNeeded(list, { silent: true, showDefaultToast: true });
    // === 新增:检测是否无饼干,立即弹出登录提示 ===
    const $display = $('#current-cookie-display');
    const $dropdown = $('#cookie-dropdown');
    if (
      !$dropdown.children().length ||
      ($display.length && $display.text().trim() === '无饼干')
    ) {
      showLoginPrompt();
    }
    // 单击下拉项即切换饼干
    $('#cookie-dropdown').on('change', function(){
      const sel = $(this).val();
      const l = getCookiesList();
      if(!Object.keys(l).length) return showLoginPrompt();
      if(!sel) return toast('请选择饼干', 0, { type: 'error' });
      // focusBackNow:选择瞬间即交还焦点,不等 switch_cookie 的网络往返
      l[sel] ? switch_cookie(l[sel], { focusBackNow: true }) : toast('饼干信息无效', 0, { type: 'error' });
      scheduleCookieDropdownFocusRestore(this, 120);
      if (!this.__focusBackTarget) delete this.__openedValue;
    });
    $('#cookie-dropdown').on('click', function(){
      if (!this.__focusBackTarget) return;
      const openedValue = this.__openedValue;
      if (openedValue == null) return;
      if (String(this.value) === String(openedValue)) {
        scheduleCookieDropdownFocusRestore(this, 120);
      }
    });
    $('#cookie-dropdown').on('keydown', function(e){
      if (!(e && (e.key === ' ' || e.code === 'Space'))) return;
      if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) return;
      e.preventDefault();
      e.stopPropagation();
      $(this).trigger('change');
    });
    $('#cookie-dropdown').on('blur', function(){
      if (this.__focusBackTimer) {
        clearTimeout(this.__focusBackTimer);
        this.__focusBackTimer = null;
      }
      delete this.__focusBackTarget;
      delete this.__openedValue;
    });
    // 刷新按钮
    $('#refresh-cookie').on('click', e=>{
      e.preventDefault();
      // 用户手动点击“刷新”:若未登录,则作为“手动弹出”强制提示,不受“不再提醒”影响
      window.__loginPromptShown = false;
      const _rcBtn = e.currentTarget;
      if (_rcBtn) _rcBtn.classList.add('xdex-icon-loading');
      refreshCookies(() => { if (_rcBtn) _rcBtn.classList.remove('xdex-icon-loading'); }, true, { manualPrompt: true });
    });
  }

  /* --------------------------------------------------
   * tag 6. 页面增强:页首页码 / 关闭水印 / 预览区真实饼干 / 隐藏无标题+无名氏+版规
   * -------------------------------------------------- */
  // ---- 旧版 enablePaginationDuplication(仅克隆 + 末页补全)----
  // function enablePaginationDuplication  (){
  //   const pags = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
  //   if(!pags.length) return;
  //   pags.forEach(pag => {
  //     const tit = document.querySelector('h2.h-title');
  //     if(!tit || !pag) return;
  //     const clone = pag.cloneNode(true);
  //     tit.parentNode.insertBefore(clone, tit.nextSibling);
  //     processPagination(clone);
  //   });
  //   const observer = new MutationObserver(mutations => {
  //     mutations.forEach(m => {
  //       m.addedNodes.forEach(node => {
  //         if(node.nodeType === 1){
  //           if(node.matches && node.matches('ul.uk-pagination.uk-pagination-left.h-pagination')){
  //             processPagination(node);
  //           }
  //           const innerPags = node.querySelectorAll?.('ul.uk-pagination.uk-pagination-left.h-pagination');
  //           innerPags?.forEach(p => processPagination(p));
  //         }
  //       });
  //     });
  //   });
  //   observer.observe(document.body, { childList: true, subtree: true });
  // }

  // ---- 旧版 rebuildPaginationPages ----
  // function rebuildPaginationPages(pag) {
  //   const totalPages = parseInt(pag.getAttribute('data-last-page'), 10);
  //   if (!totalPages || totalPages < 1) return;
  //   const activeLi = pag.querySelector('li.uk-active');
  //   if (!activeLi) return;
  //   const activeEl = activeLi.querySelector('span, a');
  //   if (!activeEl) return;
  //   const activeHref = activeEl.getAttribute('href') || '';
  //   const currentMatch = activeHref.match(/page=(\d+)/);
  //   const currentPage = currentMatch ? parseInt(currentMatch[1], 10) : 1;
  //   if (!currentPage) return;
  //   const anyLink = pag.querySelector('a[href*="page="]');
  //   if (!anyLink) return;
  //   const anyHref = anyLink.getAttribute('href') || '';
  //   const urlBase = anyHref.replace(/page=\d+/, 'page=');
  //   if (!urlBase) return;
  //   const WINDOW = 7;
  //   const HALF = Math.floor(WINDOW / 2);
  //   let start = Math.max(1, currentPage - HALF);
  //   let end = Math.min(totalPages, start + WINDOW - 1);
  //   start = Math.max(1, end - WINDOW + 1);
  //   const allLi = Array.from(pag.children);
  //   let prevPageLi = null;
  //   let nextPageLi = null;
  //   for (const li of allLi) {
  //     const text = (li.textContent || '').trim();
  //     if (text === '上一页') prevPageLi = li;
  //     else if (text === '下一页') nextPageLi = li;
  //   }
  //   if (!prevPageLi || !nextPageLi) return;
  //   let removing = false;
  //   for (const li of allLi) {
  //     if (li === prevPageLi) { removing = true; continue; }
  //     if (li === nextPageLi) break;
  //     if (removing) li.remove();
  //   }
  //   const fragment = document.createDocumentFragment();
  //   for (let p = start; p <= end; p++) {
  //     const li = document.createElement('li');
  //     if (p === currentPage) {
  //       li.className = 'uk-active';
  //       const span = document.createElement('span');
  //       span.setAttribute('href', urlBase + p);
  //       span.textContent = String(p);
  //       li.appendChild(span);
  //     } else {
  //       const a = document.createElement('a');
  //       a.setAttribute('href', urlBase + p);
  //       a.setAttribute('achecked', '1');
  //       a.textContent = String(p);
  //       li.appendChild(a);
  //     }
  //     fragment.appendChild(li);
  //   }
  //   pag.insertBefore(fragment, nextPageLi);
  // }

  function parsePaginationPageNum(href) {
    if (!href) return null;
    const value = String(href);
    let m = value.match(/[?&]page=(\d+)/);
    if (m) return parseInt(m[1], 10);
    // 支持 /page/N.html 与裸 /page/N(如 /t/{id}/page/3)
    m = value.match(/\/page\/(\d+)(?:\.html)?(?:[?#]|$)/);
    if (m) return parseInt(m[1], 10);
    m = value.match(/\/page\/(\d+)(?:\.html)?$/);
    if (m) return parseInt(m[1], 10);
    return null;
  }
  function buildPaginationHref(sampleHref, pageNum) {
    const href = String(sampleHref || '');
    if (/[?&]page=\d+/.test(href)) return href.replace(/page=\d+/, `page=${pageNum}`);
    if (/\/page\/\d+\.html/.test(href)) return href.replace(/\/page\/\d+\.html/, `/page/${pageNum}.html`);
    if (isBoardPaginationContext() || PageType.isTimelinePage()) {
      const base = location.pathname.replace(/\/page\/\d+\.html$/, '').replace(/\.html$/, '');
      return `${base}/page/${pageNum}.html`;
    }
    return href;
  }
  function isBoardPaginationContext() {
    return /^\/f\//.test(location.pathname);
  }
  // 重建分页栏页码:显示 7 个页码,当前页居中(边界时左/右对齐)
  function rebuildPaginationPages(pag) {
    // 1) 提取分页链接样本:支持串内 ?page=N,也支持版块页 /page/N.html
    const anyLink = Array.from(pag.querySelectorAll('a[href], span[href]')).find(el => parsePaginationPageNum(el.getAttribute('href')));
    if (!anyLink) return;
    const sampleHref = anyLink.getAttribute('href') || '';
    // 2) 提取当前页码:优先 uk-active,回退到 data-cloned-page,再回退到上一页+1
    // (先提取当前页,后面计算总页数需要用到)
    let currentPage = 0;
    const activeLi = pag.querySelector('li.uk-active');
    if (activeLi) {
      const activeEl = activeLi.querySelector('span, a');
      if (activeEl) {
        const activeHref = activeEl.getAttribute('href') || '';
        currentPage = parsePaginationPageNum(activeHref) || 0;
        if (!currentPage && /^\d+$/.test((activeEl.textContent || '').trim())) currentPage = parseInt((activeEl.textContent || '').trim(), 10);
      }
    }
    if (!currentPage) {
      currentPage = parseInt(pag.getAttribute('data-cloned-page'), 10) || 0;
    }
    if (!currentPage) {
      const prevLink = Array.from(pag.querySelectorAll('a')).find(a => (a.textContent || '').trim() === '上一页');
      if (prevLink) {
        const prevHref = prevLink.getAttribute('href') || '';
        const prevPage = parsePaginationPageNum(prevHref);
        currentPage = prevPage ? prevPage + 1 : 0;
      }
    }
    if (!currentPage) currentPage = 1;
    // 3) 提取总页数(多级回退)
    let totalPages = 0;
    // 回退1:末页链接的 href
    const lastLink = Array.from(pag.querySelectorAll('a')).find(a => (a.textContent || '').trim().startsWith('末页'));
    const hasExplicitLastPage = !!lastLink;
    if (lastLink) {
      const lastHref = lastLink.getAttribute('href') || '';
      totalPages = parsePaginationPageNum(lastHref) || 0;
    }
    // 回退2:data-last-page(无缝翻页的属性,可能是翻页前的页码而非真正的总页数)
    if (!totalPages || totalPages < 1) {
      totalPages = parseInt(pag.getAttribute('data-last-page'), 10) || 0;
    }
    // 回退3:从所有页码链接中取最大值
    if (!totalPages || totalPages < 1) {
      let maxPage = 0;
      pag.querySelectorAll('a[href], span[href]').forEach(el => {
        const n = parsePaginationPageNum(el.getAttribute('href') || '');
        if (n) maxPage = Math.max(maxPage, n);
      });
      totalPages = maxPage;
    }
    // 版块页最多展示最近 100 页;/f/ 通常没有“末页”链接,不能把当前 DOM 的 1/2/3 误判为总页数
    if (isBoardPaginationContext()) {
      totalPages = hasExplicitLastPage && totalPages ? Math.min(100, totalPages) : 100;
      if (currentPage > totalPages) totalPages = Math.min(100, currentPage);
    }
    // 关键修正:如果"下一页"被禁用(uk-disabled),说明当前就是末页
    const nextPageCandidate = Array.from(pag.children).find(li => (li.textContent || '').trim() === '下一页');
    if (nextPageCandidate && nextPageCandidate.classList.contains('uk-disabled')) {
      totalPages = Math.max(totalPages, currentPage);
    }
    // 兜底:总页数不能小于当前页
    if (totalPages < currentPage) totalPages = currentPage;
    if (!totalPages) return;
    // console.debug('[rebuildPaginationPages]', { totalPages, currentPage, sampleHref });
    // 4) 计算页码窗口
    const WINDOW = 7;
    const HALF = Math.floor(WINDOW / 2);
    let start = Math.max(1, currentPage - HALF);
    let end = Math.min(totalPages, start + WINDOW - 1);
    start = Math.max(1, end - WINDOW + 1);
    // 幂等检查:如果上一页/下一页之间已经是目标页码,就不要再改 DOM,避免 observer 自触发循环
    const currentPageItems = Array.from(pag.children)
      .filter(li => {
        const text = (li.textContent || '').trim();
        return /^\d+$/.test(text);
      })
      .map(li => ({ text: (li.textContent || '').trim(), active: li.classList.contains('uk-active') }));
    const expectedPageItems = [];
    for (let p = start; p <= end; p++) expectedPageItems.push({ text: String(p), active: p === currentPage });
    const alreadyMatches = currentPageItems.length === expectedPageItems.length
      && currentPageItems.every((item, idx) => item.text === expectedPageItems[idx].text && item.active === expectedPageItems[idx].active);
    if (alreadyMatches) {
      pag.setAttribute('data-xdex-rebuilt', '1');
      return;
    }
    // 5) 定位上一页/下一页按钮
    const allLi = Array.from(pag.children);
    let prevPageLi = null;
    let nextPageLi = null;
    for (const li of allLi) {
      const text = (li.textContent || '').trim();
      if (text === '上一页') prevPageLi = li;
      else if (text === '下一页') nextPageLi = li;
    }
    if (!prevPageLi || !nextPageLi) return;
    // 6) 移除上一页和下一页之间的旧页码 <li>
    let removing = false;
    for (const li of allLi) {
      if (li === prevPageLi) { removing = true; continue; }
      if (li === nextPageLi) break;
      if (removing) li.remove();
    }
    // 7) 生成新页码 <li> 并插入
    const fragment = document.createDocumentFragment();
    for (let p = start; p <= end; p++) {
      const li = document.createElement('li');
      if (p === currentPage) {
        li.className = 'uk-active';
        const span = document.createElement('span');
        span.setAttribute('href', buildPaginationHref(sampleHref, p));
        span.textContent = String(p);
        li.appendChild(span);
      } else {
        const a = document.createElement('a');
        a.setAttribute('href', buildPaginationHref(sampleHref, p));
        a.setAttribute('achecked', '1');
        a.textContent = String(p);
        li.appendChild(a);
      }
      fragment.appendChild(li);
    }
    pag.insertBefore(fragment, nextPageLi);
    // 标记已重建,避免延迟重扫或 MutationObserver 重复处理
    pag.setAttribute('data-xdex-rebuilt', '1');
  }
  function enablePaginationDuplication(cloneTitlePagination = true){
    // 获取所有分页栏,而不是只获取一个
    const pags = Array.from(document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination'));
    if(!pags.length) return;
    const tit = cloneTitlePagination ? document.querySelector('h2.h-title') : null;
    let titlePag = null;
    if (tit) {
      // early pass 和 ready 兜底都可能运行;标题后页码栏全页只保留一条
      const titlePags = pags.filter(pag => pag.dataset.xdexTitleClone === '1' || pag.previousElementSibling === tit);
      titlePags.forEach((pag, idx) => {
        if (idx === 0) {
          titlePag = pag;
          titlePag.dataset.xdexTitleClone = '1';
        } else {
          console.info('[X岛-EX][页首页码] 去重:移除多余标题页码栏 (prev=' + (pag.previousElementSibling ? pag.previousElementSibling.tagName + '.' + String(pag.previousElementSibling.className).split(' ')[0] : 'null') + ')');
          pag.remove();
        }
      });
    }
    const sourcePags = pags.filter(pag => pag.isConnected && pag.dataset.xdexTitleClone !== '1' && pag.previousElementSibling !== tit);
    sourcePags.forEach(pag => {
      // 重建页码(显示 7 个页码,当前页居中)
      rebuildPaginationPages(pag);
      // 末页补全
      processPagination(pag);
    });
    if (tit && !titlePag && sourcePags.length) {
      // 克隆分页栏并插入标题后
      titlePag = sourcePags[0].cloneNode(true);
      titlePag.dataset.xdexTitleClone = '1';
      tit.parentNode.insertBefore(titlePag, tit.nextSibling);
      console.info('[X岛-EX][页首页码] 已创建页首页码栏 (来源=' + (sourcePags[0].previousElementSibling ? sourcePags[0].previousElementSibling.tagName : '?') + ')');
    }
    if (titlePag && titlePag.isConnected) {
      rebuildPaginationPages(titlePag);
      // 对克隆分页栏执行末页补全(页码已随克隆复制)
      processPagination(titlePag);
    }
    // 延迟重建:防止其他功能(如无缝翻页初始化)在之后覆盖分页栏
    function rescanAndRebuild() {
      document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination:not([data-xdex-rebuilt])').forEach(pag => {
        rebuildPaginationPages(pag);
        processPagination(pag);
        pag.setAttribute('data-xdex-rebuilt', '1');
      });
    }
    [500, 1500].forEach(ms => setTimeout(rescanAndRebuild, ms));
    // 监听 DOM 变化,自动处理后续新增/替换的分页栏
    // 用防抖避免 rebuild 改 DOM 后触发 observer 造成无限循环
    const pagDebounceTimers = new WeakMap();
    const observer = new MutationObserver(mutations => {
      const targets = new Set();
      mutations.forEach(m => {
        m.addedNodes.forEach(node => {
          if(node.nodeType !== 1) return;
          // 情况1:新增的节点本身是分页栏
          if(node.matches && node.matches('ul.uk-pagination.uk-pagination-left.h-pagination')){
            targets.add(node);
            return;
          }
          // 情况2:新增的节点内部包含分页栏
          const innerPags = node.querySelectorAll?.('ul.uk-pagination.uk-pagination-left.h-pagination');
          innerPags?.forEach(p => targets.add(p));
          // 情况3:新增的节点是分页栏的子节点(如 <li> 被替换),向上查找父分页栏
          const parentPag = node.closest?.('ul.uk-pagination.uk-pagination-left.h-pagination');
          if (parentPag) targets.add(parentPag);
        });
      });
      // 对每个受影响的分页栏做防抖重建(50ms 内重复变化只触发一次)
      targets.forEach(pag => {
        if (pagDebounceTimers.has(pag)) clearTimeout(pagDebounceTimers.get(pag));
        pagDebounceTimers.set(pag, setTimeout(() => {
          pagDebounceTimers.delete(pag);
          if (pag.dataset.xdexTitleClone === '1' || pag.previousElementSibling?.matches?.('h2.h-title')) {
            const tit = document.querySelector('h2.h-title');
            const titlePags = Array.from(document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination'))
              .filter(p => p.dataset.xdexTitleClone === '1' || p.previousElementSibling === tit);
            titlePags.forEach((p, idx) => {
              if (idx === 0) {
                p.dataset.xdexTitleClone = '1';
                rebuildPaginationPages(p);
                processPagination(p);
              } else {
                p.remove();
              }
            });
            return;
          }
          rebuildPaginationPages(pag);
          processPagination(pag);
        }, 50));
      });
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }
  // 专门处理“末页”按钮的函数
  function processPagination(pag){
    pag.querySelectorAll('a').forEach(a => {
      if(a.textContent.trim().startsWith('末页')){
        // 如果已经有页码则跳过
        if(/\(\d+\)$/.test(a.textContent.trim())) return;
        // 第一种格式:?page=13
        let m = a.href.match(/page=(\d+)/);
        // 第二种格式:/page/6.html
        if(!m){
          m = a.href.match(/\/page\/(\d+)\.html/);
        }
        if(m){
          a.textContent = `末页(${m[1]})`;
        }
      }
    });
  }
  const disableWatermark = () => {
    const c = document.querySelector('input[type="checkbox"][name="water"][value="true"]');
    if(c) c.checked = false;
  };
  function updatePreviewCookieId(){
    if(!$('.h-preview-box').length) return;
    const cur=getCurrentCookie();
    const $uid = $('.h-preview-box .h-threads-info-uid');
    const listUnavailable = isCookieListUnavailable(getCookiesList());
    if (listUnavailable) {
      const likely = getLikelyActiveCookie();
      const name = likely && likely.name ? abbreviateName(likely.name) : '未知';
      $uid.text(`已退出登录,无法获取列表,当前饼干可能为:${name}`).css('color', 'red');
      getCookieMatchedByCurrentUserhash().then(matched => {
        if (!matched || !matched.name || !isCookieListUnavailable(getCookiesList())) return;
        $uid.text(`已退出登录,无法切换饼干,当前实际作用饼干为:${abbreviateName(matched.name)}`).css('color', 'red');
        if (typeof markAllCookies === 'function') {
          try {
            const cfg = getFilterConfig();
            markAllCookies(cfg.markedGroups || [], $('.h-preview-box')[0]);
          } catch (e) {}
        }
      });
    } else {
      const name=cur&&cur.name?abbreviateName(cur.name):'无饼干';
      $uid.text('ID:'+name).css('color', '');
      cookieListUnavailableState = false;
    }
    // 切换饼干后更新标记背景色
    if (typeof markAllCookies === 'function') {
      try {
        const cfg = getFilterConfig();
        markAllCookies(cfg.markedGroups || [], $('.h-preview-box')[0]);
      } catch (e) {}
    }
  }
  function hideEmptyTitleAndEmail(root){
    const $root = root ? $(root) : null;
    const $titles = $root
      ? $root.find('.h-threads-info-title').add($root.filter('.h-threads-info-title'))
      : $('.h-threads-info-title');
    const $emails = $root
      ? $root.find('.h-threads-info-email').add($root.filter('.h-threads-info-email'))
      : $('.h-threads-info-email');
    $titles.each(function(){
      if ($(this).text().trim() === '无标题' && this.style.display !== 'none') {
        this.style.display = 'none';
      }
    });
    $emails.each(function(){
      if ($(this).text().trim() === '无名氏' && this.style.display !== 'none') {
        this.style.display = 'none';
      }
    });
  }
  function getEarlyStartupConfig() {
    try {
      return Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
    } catch (e) {
      return SettingPanel.defaults;
    }
  }
  function collapseEarlyStartupBlocks(root, cfg) {
    if (!cfg || !cfg.hideEmptyTitleEmail || typeof Utils === 'undefined' || typeof Utils.collapse !== 'function') return;
    const $root = root ? $(root) : $(document);
    Utils.collapse($root.find('.h-forum-header').addBack('.h-forum-header'), '『版规』');
    Utils.collapse($root.find('form[action="/Home/Forum/doReplyThread.html"]').addBack('form[action="/Home/Forum/doReplyThread.html"]'), '『回复』');
    Utils.collapse($root.find('form[action="/Home/Forum/doPostThread.html"]').addBack('form[action="/Home/Forum/doPostThread.html"]'), '『发串』');
  }
  // === 全局:实时定位当前真正在屏幕上的预览框(浮窗内优先)===
  function getLivePreviewBox() {
    return document.querySelector('.qp-body .h-preview-box')
        || document.querySelector('#h-post-form .h-preview-box')
        || document.querySelector('.h-preview-box');
  }
  function looksLikeHeifFile(file) {
    if (!file) return false;
    const type = String(file.type || '').toLowerCase();
    if (type === 'image/heic' || type === 'image/heif' || type === 'image/heic-sequence' || type === 'image/heif-sequence') {
      return true;
    }
    const name = String(file.name || '');
    return /\.(heic|heif|hif)$/i.test(name);
  }
  let lastHeifPreviewTipKey = '';
  let lastHeifPreviewTipFile = null;
  let lastHeifPreviewTipAt = 0;
  let heifPreviewTipSeq = 0;
  // 清除附件(清空按钮 / 原生 input 清空 / 发送成功后清空)时必须调用,
  // 否则同一个 HEIC 文件重新附带会被去重键挡住、不再提示。
  function resetHeifPreviewTipState() {
    lastHeifPreviewTipKey = '';
    lastHeifPreviewTipFile = null;
    lastHeifPreviewTipAt = 0;
  }
  function maybeToastHeifPreviewUnsupported(file) {
    if (!file) {
      resetHeifPreviewTipState();
      return;
    }
    const key = [file.name || '', file.size || 0, file.lastModified || 0].join('|');
    const now = Date.now();
    if (key === lastHeifPreviewTipKey) {
      // 同一次附带动作内会被多次调用(粘贴处理器 → 写入 input → change → 初始化补渲染),
      // 这些调用可能拿到同一个 File 的不同实例:同一实例直接去重,不同实例只在短窗口内去重。
      if (file === lastHeifPreviewTipFile) return;
      if (now - lastHeifPreviewTipAt < 1200) return;
    }
    lastHeifPreviewTipKey = key;
    lastHeifPreviewTipFile = file;
    lastHeifPreviewTipAt = now;
    toast('当前不支持 HEIF/HEIC 预览(发送时将自动转为PNG)', 2500);
  }
  function notifyHeifPreviewUnsupportedIfNeeded(file, seq) {
    if (!file) {
      resetHeifPreviewTipState();
      return;
    }
    // 快路径:MIME/扩展名可立刻提示
    if (looksLikeHeifFile(file)) {
      if (seq === heifPreviewTipSeq) maybeToastHeifPreviewUnsupported(file);
      return;
    }
    // 慢路径:无扩展名/MIME 时用魔数确认(与提交侧 detectImageFormat 一致)
    if (typeof detectImageFormat !== 'function') return;
    Promise.resolve(detectImageFormat(file)).then((fmt) => {
      if (seq !== heifPreviewTipSeq) return;
      if (fmt === 'heic') maybeToastHeifPreviewUnsupported(file);
    }).catch(() => {});
  }
  // === 全局:把图片文件渲染到当前活跃预览框 ===
  // 不依赖任何闭包快照,每次实时查询预览框与所在位置,避免 REPLY 浮窗
  // open() 早于 enhanceIsland 初始化时,粘贴图片更新到看不见的框。
  function updatePreviewImageFromFile(file) {
    // HEIF 预览:浏览器 <img>+blob 通常无法解码,仅提示;不改 input 中的原文件
    const tipSeq = ++heifPreviewTipSeq;
    if (file) notifyHeifPreviewUnsupportedIfNeeded(file, tipSeq);
    else resetHeifPreviewTipState();
    const liveBox = getLivePreviewBox();
    if (!liveBox) return;
    const $liveBox = $(liveBox);
    const imgEl = $liveBox.find('.h-threads-img')[0];
    const imgLink = $liveBox.find('.h-threads-img-a')[0];
    const toolLarge = $liveBox.find('.h-threads-img-tool-large')[0];
    const imgBox = $liveBox.find('.h-threads-img-box')[0];
    if (!imgEl) return;
    if (imgEl.dataset.prevObjectUrl) {
      URL.revokeObjectURL(imgEl.dataset.prevObjectUrl);
      delete imgEl.dataset.prevObjectUrl;
    }
    if (file) {
      const objectUrl = URL.createObjectURL(file);
      imgEl.src = objectUrl;
      imgEl.dataset.prevObjectUrl = objectUrl;
      imgEl.style.display = 'block';
      if (imgLink) imgLink.href = objectUrl;
      if (toolLarge) toolLarge.href = objectUrl;
      if (imgBox) imgBox.classList.remove('h-active');
      imgEl.dataset.rotateIndex = '0';
      imgEl.style.transform = '';
      imgEl.style.top = '0px';
      imgEl.style.left = '0px';
      imgEl.style.width = 'auto';
      imgEl.style.height = 'auto';
      const isInOverlay = !!$liveBox.closest('.qp-body').length;
      if (isInOverlay) {
        const wrapEl = $liveBox.closest('.qp-content-wrap')[0];
        if (wrapEl) {
          const wrapWidth = wrapEl.getBoundingClientRect().width;
          imgEl.style.maxWidth = (wrapWidth / 3) + 'px';
          imgEl.style.height = 'auto';
        }
      } else {
        const formEl = document.querySelector('#h-post-form form');
        if (formEl) {
          const formWidth = formEl.getBoundingClientRect().width;
          imgEl.style.maxWidth = (formWidth / 2) + 'px';
          imgEl.style.height = 'auto';
        }
      }
    } else {
      imgEl.removeAttribute('src');
      imgEl.style.display = 'none';
      imgEl.style.maxWidth = '';
      imgEl.style.width = '';
      imgEl.style.height = '';
      imgEl.style.transform = '';
      imgEl.style.top = '0px';
      imgEl.style.left = '0px';
      delete imgEl.dataset.rotateIndex;
      if (imgLink) {
        imgLink.removeAttribute('href');
        imgLink.style.height = '';
      }
      if (toolLarge) toolLarge.href = 'javascript:;';
      if (imgBox) imgBox.classList.remove('h-active');
    }
  }
  // === 全局:绑定粘贴图片预览(幂等),供 enhanceIsland 与 REPLY 浮窗 open() 共用 ===
  function bindPasteImagePreviewOnce() {
    if (bindPasteImagePreviewOnce.__bound) return;
    bindPasteImagePreviewOnce.__bound = true;
    document.addEventListener('paste', function (e) {
      const items = (e.clipboardData || e.originalEvent?.clipboardData)?.items || [];
      if (!items.length) return;
      let file = null;
      for (const it of items) {
        if (it.kind === 'file') {
          const f = it.getAsFile();
          if (f && f.type.startsWith('image/')) {
            file = f;
            break;
          }
        }
      }
      if (!file) return;
      // 优先取浮窗内的 file input,否则取原表单的
      const fileInput = document.querySelector('.qp-body input[type="file"][name="image"]')
        || document.querySelector('#h-post-form input[type="file"][name="image"]')
        || document.querySelector('input[type="file"][name="image"]');
      if (fileInput) {
        try {
          const dt = new DataTransfer();
          dt.items.add(file);
          fileInput.files = dt.files;
        } catch (_) {}
      }
      updatePreviewImageFromFile(file);
      if (fileInput) {
        try { fileInput.dispatchEvent(new Event('change', { bubbles: true })); } catch (_) {}
      }
    }, true);
  }
  const PREVIEW_ID_POOL = ['0712','0711','0412','1002','1210','0520','1105','0217','0831','1227','1227','0130','0812','1126','0722','0119','0520','0630','0210','1110','1227'];
  let previewPlaceholderId = null;
  function getPreviewPlaceholderId(reuse) {
    // reuse=true(早期注入与正式绑定):复用同一编号,避免替换时视觉割裂;reuse=false(发送后重建/重置):重新随机
    if (!reuse || !previewPlaceholderId) {
      previewPlaceholderId = PREVIEW_ID_POOL[Math.floor(Math.random() * PREVIEW_ID_POOL.length)];
    }
    return previewPlaceholderId;
  }
  function buildEnhanceIslandPreviewHtml(early) {
    // 从 cookie-switcher 里取当前饼干
    const cookieDisplay = document.querySelector('#h-post-form #current-cookie-display');
    const cookieText = cookieDisplay ? cookieDisplay.textContent.trim() : '--';
    return `
      <div class="h-preview-box"${early ? ' data-xdex-early-preview="1"' : ''}>
        <div class="h-threads-item">
          <div class="h-threads-item-replies">
            <div class="h-threads-item-reply" style="width:100%">
              <div class="h-threads-item-reply-main">
                <div class="h-threads-img-box">
                  <div class="h-threads-img-tool uk-animation-slide-top">
                    <span class="h-threads-img-tool-btn h-threads-img-tool-small uk-button-link"><i class="uk-icon-minus"></i>收起</span>
                    <a href="javascript:;" class="h-threads-img-tool-btn h-threads-img-tool-large uk-button-link"><i class="uk-icon-search-plus"></i>查看大图</a>
                    <span class="h-threads-img-tool-btn h-threads-img-tool-left uk-button-link"><i class="uk-icon-reply"></i>向左旋转</span>
                    <span class="h-threads-img-tool-btn h-threads-img-tool-right uk-button-link"><i class="uk-icon-share"></i>向右旋转</span>
                  </div>
                  <a class="h-threads-img-a"><img src="" align="left" border="0" hspace="20" class="h-threads-img"></a>
                </div>
                <div class="h-threads-info">
                  <span class="h-threads-info-title"></span>
                  <span class="h-threads-info-email"></span>
                  <span class="h-threads-info-createdat">2013-07-11(六)12:07:12</span>
                  <span class="h-threads-info-uid">ID:${cookieText}</span>
                  <!-- <span class="h-threads-info-report-btn">
                    [<a href="/f/值班室" target="_blank">举报</a>]
                  </span> -->
                  <a class="h-threads-info-id" href="javascript:;" style="cursor: default;">No.${getPreviewPlaceholderId(early)}</a>
                </div>
                <div class="h-threads-content"></div>
              </div>
            </div>
          </div>
        </div>
      </div>`;
  }
  function applyEarlyPreviewBoxStyle(previewEl) {
    if (!previewEl) return;
    previewEl.style.width = '100%';
    previewEl.style.boxSizing = 'border-box';
    const content = previewEl.querySelector('.h-threads-content');
    if (content) {
      content.style.overflowWrap = 'break-word';
      content.style.wordBreak = 'break-word';
      content.style.whiteSpace = 'normal';
    }
  }
  function tryInsertPreviewBoxEarly(formOpt) {
    if (!document.body) return false;
    // 已有预览框则复用(浮窗内优先)
    const existing = document.querySelector('.qp-body .h-preview-box')
      || document.querySelector('.h-preview-box');
    if (existing) return true;
    const form = formOpt
      || document.querySelector('#h-post-form form')
      || document.querySelector('form[action="/Home/Forum/doReplyThread.html"]')
      || document.querySelector('form[action="/Home/Forum/doPostThread.html"]');
    if (!form || !form.parentNode) return false;
    form.insertAdjacentHTML('afterend', buildEnhanceIslandPreviewHtml(true));
    const previewEl = form.nextElementSibling && form.nextElementSibling.classList?.contains('h-preview-box')
      ? form.nextElementSibling
      : document.querySelector('.h-preview-box');
    applyEarlyPreviewBoxStyle(previewEl);
    if (typeof updatePreviewCookieId === 'function') {
      try { updatePreviewCookieId(); } catch (e) {}
    }
    return !!previewEl;
  }
  function tryEnablePaginationDuplicationEarly(root) {
    const scope = root && root.querySelectorAll ? root : document;
    const tit = document.querySelector('h2.h-title');
    const pags = [];
    if (scope.matches && scope.matches('ul.uk-pagination.uk-pagination-left.h-pagination')) pags.push(scope);
    scope.querySelectorAll?.('ul.uk-pagination.uk-pagination-left.h-pagination').forEach(p => pags.push(p));
    if (!pags.length) return false;
    let titlePag = null;
    if (tit) {
      const titlePags = pags.filter(pag => pag.dataset.xdexTitleClone === '1' || pag.previousElementSibling === tit);
      titlePags.forEach((pag, idx) => {
        if (idx === 0) {
          titlePag = pag;
          titlePag.dataset.xdexTitleClone = '1';
        } else {
          pag.remove();
        }
      });
    }
    let changed = false;
    pags.forEach(pag => {
      if (!pag.isConnected) return;
      try { if (typeof rebuildPaginationPages === 'function') rebuildPaginationPages(pag); } catch (e) {}
      try { if (typeof processPagination === 'function') processPagination(pag); } catch (e) {}
      if (!tit || titlePag || pag.dataset.xdexTitleClone === '1' || pag.previousElementSibling === tit) return;
      const clone = pag.cloneNode(true);
      clone.dataset.xdexTitleClone = '1';
      tit.parentNode.insertBefore(clone, tit.nextSibling);
      try { if (typeof processPagination === 'function') processPagination(clone); } catch (e) {}
      titlePag = clone;
      changed = true;
    });
    return changed;
  }
  function runCriticalVisualEnhancements(root, cfg) {
    const liveCfg = cfg || getEarlyStartupConfig();
    try {
      if (liveCfg.hideEmptyTitleEmail && typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail(root);
    } catch (e) {}
    try {
      if (liveCfg.hideEmptyTitleEmail) collapseEarlyStartupBlocks(root, liveCfg);
    } catch (e) {}
    try {
      if (typeof highlightPO === 'function') highlightPO(root);
    } catch (e) {}
    try {
      if (typeof refreshFilterDisplay === 'function') refreshFilterDisplay(liveCfg, root || document);
    } catch (e) {}
    try {
      if (liveCfg.enableRelativeTime && typeof formatDateStrOnPage === 'function') formatDateStrOnPage(root || document);
    } catch (e) {}
    try {
      if (typeof autoSelectReportReason === 'function') autoSelectReportReason(root || document);
    } catch (e) {}
    try {
      if (liveCfg.enablePaginationDuplication) tryEnablePaginationDuplicationEarly(root);
    } catch (e) {}
    try {
      if (liveCfg.updatePreviewCookie) tryInsertPreviewBoxEarly();
    } catch (e) {}
    try {
      if (typeof tryReplaceRightSidebarEarly === 'function') tryReplaceRightSidebarEarly();
    } catch (e) {}
    try {
      // tag 30:测试板块侧边栏入口(每次页面加载 / 局部增强都会补挂,含观察器兜底)
      if (typeof installTestBoardSidebarLinkObserver === 'function') installTestBoardSidebarLinkObserver();
      // tag 30:测试版主串列表守护(抢在站点内联脚本移除前装好 removedNodes 守护器)
      if (typeof installTestBoardListGuard === 'function') installTestBoardListGuard();
      // tag 30:测试版主串列表恢复兜底(守护器失手时网络拉取放回)
      if (typeof restoreTestBoardThreadList === 'function') restoreTestBoardThreadList();
    } catch (e) {}
    // 顶栏刷新点击:越早越好,不依赖 batch2
    try {
      if (typeof installOverrideTopImageClickEarly === 'function') installOverrideTopImageClickEarly();
      else if (typeof overrideTopImageClick === 'function') overrideTopImageClick();
    } catch (e) {}
  }
  function runEarlyStartupPass(root) {
    runCriticalVisualEnhancements(root, getEarlyStartupConfig());
  }
  function installEarlyCriticalVisualPass() {
    const startedAt = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
    const duration = 800;
    let rafId = 0;
    function now() { return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now(); }
    function pass() {
      rafId = 0;
      try { runCriticalVisualEnhancements(document, getEarlyStartupConfig()); } catch (e) {}
      if (now() - startedAt < duration) {
        rafId = requestAnimationFrame(pass);
      }
    }
    pass();
    setTimeout(() => { if (rafId) cancelAnimationFrame(rafId); }, duration + 250);
  }
  function getEnhanceIslandOriginalTitle() {
    const root = document.documentElement;
    if (!root) return document.title;
    if (!root.dataset.xdexOriginalTitle) {
      root.dataset.xdexOriginalTitle = document.title;
    }
    return root.dataset.xdexOriginalTitle || document.title;
  }
  function selectEnhanceIslandTitleText() {
    const titleEl = document.querySelector('.h-threads-list .h-threads-item-main .h-threads-info .h-threads-info-title');
    const contentEl = document.querySelector('.h-threads-list .h-threads-item-main .h-threads-content');
    if (!contentEl) return '';
    const titleText = (titleEl?.textContent || '').trim();
    if (titleText && titleText !== '无标题') return titleText;
    const walker = document.createTreeWalker(contentEl, NodeFilter.SHOW_ELEMENT);
    let node = contentEl;
    while (node) {
      try {
        if (window.getComputedStyle(node).color === 'rgb(255, 0, 0)') {
          const redSegment = node.textContent.replace(/^[=\s+]+|[=\s+]+$/g, '');
          if (redSegment) return redSegment;
        }
      } catch (_) {}
      node = walker.nextNode();
    }
    const lines = (contentEl.innerText || contentEl.textContent || '').split('\n');
    for (let line of lines) {
      line = line.trim();
      if (line) return line;
    }
    return '';
  }
  function applyEarlyEnhanceIslandAutoTitle() {
    if (!isEnhanceIslandAutoTitlePage()) return false;
    const titleText = selectEnhanceIslandTitleText();
    if (!titleText) return false;
    const pathBlocks = window.location.pathname.split('/').splice(1);
    const searchParams = new URLSearchParams(window.location.search || '');
    const page = pathBlocks[0] === 'Forum'
      ? (pathBlocks[5]?.replace(/\.html$/, '') || 1)
      : (searchParams.get('page') || 1);
    const titleEl = document.querySelector('title');
    if (!titleEl) return false;
    titleEl.textContent = `${titleText} - ${getEnhanceIslandOriginalTitle()} - page ${page}`;
    return true;
  }
  function installEarlyEnhanceIslandAutoTitle() {
    if (!isEnhanceIslandAutoTitlePage()) return;
    let observer = null;
    let rafId = 0;
    let stopped = false;
    const stop = () => {
      stopped = true;
      if (rafId) cancelAnimationFrame(rafId);
      if (observer) observer.disconnect();
      observer = null;
    };
    const queueTitleUpdate = () => {
      if (stopped || rafId) return;
      rafId = requestAnimationFrame(() => {
        rafId = 0;
        if (stopped) return;
        if (applyEarlyEnhanceIslandAutoTitle()) stop();
      });
    };
    const startObserve = () => {
      if (stopped) return;
      const target = document.body || document.documentElement;
      if (!target || observer) return;
      observer = new MutationObserver(queueTitleUpdate);
      observer.observe(target, { childList: true, subtree: true, characterData: true });
      queueTitleUpdate();
      setTimeout(stop, 5000);
    };
    getEnhanceIslandOriginalTitle();
    if (document.body || document.documentElement) {
      startObserve();
    } else {
      const timer = setInterval(() => {
        if (document.body || document.documentElement) {
          clearInterval(timer);
          startObserve();
        }
      }, 25);
      setTimeout(() => {
        clearInterval(timer);
        stop();
      }, 5000);
    }
  }
  function installEarlyStartupObserver() {
    const relevantSelector = [
      '.h-threads-list',
      '.h-threads-item',
      '.h-threads-item-replies',
      '.h-threads-item-reply',
      '.h-threads-info-title',
      '.h-threads-info-email',
      '.h-threads-info-uid',
      '.h-threads-item-reply-icon',
      '.h-forum-header',
      'ul.uk-pagination.uk-pagination-left.h-pagination',
      'form[action="/Home/Forum/doReplyThread.html"]',
      'form[action="/Home/Forum/doPostThread.html"]',
      '#h-report-switch'
    ].join(',');
    let observer = null;
    let rafId = 0;
    let passCount = 0;
    let stopped = false;
    const stop = () => {
      stopped = true;
      if (rafId) cancelAnimationFrame(rafId);
      if (observer) observer.disconnect();
      observer = null;
    };
    const queuePass = () => {
      if (stopped || rafId) return;
      rafId = requestAnimationFrame(() => {
        rafId = 0;
        if (stopped) return;
        runEarlyStartupPass(document);
        passCount += 1;
        if (passCount >= 4) stop();
      });
    };
    const startObserve = () => {
      if (stopped) return;
      const target = document.body || document.documentElement;
      if (!target || observer) return;
      observer = new MutationObserver(mutations => {
        for (const mutation of mutations) {
          for (const node of mutation.addedNodes) {
            if (node.nodeType !== 1) continue;
            const el = node;
            if (el.matches?.(relevantSelector) || el.querySelector?.(relevantSelector)) {
              queuePass();
              return;
            }
          }
        }
      });
      observer.observe(target, { childList: true, subtree: true });
      setTimeout(stop, 1200);
    };
    runEarlyStartupPass(document);
    if (document.body || document.documentElement) {
      startObserve();
    } else {
      const timer = setInterval(() => {
        if (document.body || document.documentElement) {
          clearInterval(timer);
          startObserve();
        }
      }, 25);
      setTimeout(() => {
        clearInterval(timer);
        stop();
      }, 1200);
    }
  }
  const startupPerfDebug = (() => {
    // 现场取证开关:默认关闭(关闭时 record/mark 全是空调用,无额外开销)。
    // 开启方式(无需改源码):
    //   localStorage.setItem('xdexPerfCollect','1') 后刷新页面;或控制台执行 __xdexPerfCollectOn() 再刷新
    // 关闭:__xdexPerfCollectOff() 后刷新,或 localStorage.removeItem('xdexPerfCollect')
    let AUTO_COLLECTION_ENABLED = false;
    try {
      AUTO_COLLECTION_ENABLED = (typeof localStorage !== 'undefined' && localStorage.getItem('xdexPerfCollect') === '1');
    } catch (e) {}
    try {
      window.__xdexPerfCollectOn = () => { try { localStorage.setItem('xdexPerfCollect', '1'); } catch (e) {} console.log('[XDEX perf] 采集已开启,请刷新页面后复现操作'); };
      window.__xdexPerfCollectOff = () => { try { localStorage.removeItem('xdexPerfCollect'); } catch (e) {} console.log('[XDEX perf] 采集已关闭,请刷新页面'); };
      window.__xdexPerfCollectState = () => ({ enabled: AUTO_COLLECTION_ENABLED, report: 'window.__xdexStartupPerfReport()', reset: 'window.__xdexStartupPerfReset()', capture: 'window.__xdexPerfCapture("场景名")', list: 'window.__xdexPerfRounds()', print: 'window.__xdexPerfPrint()', clear: 'window.__xdexPerfClear()' });
    } catch (e) {}
    const LONG_TASK_MS = 50;
    const LOG_TASK_MS = 16;
    const AUTO_REPORT_AFTER_MS = 60000;
    const stats = new Map();
    const events = [];
    const browserEvents = [];
    const startedAt = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
    const target = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
    function now() {
      return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
    }
    function getStat(label) {
      if (!stats.has(label)) {
        stats.set(label, { label, count: 0, total: 0, max: 0, longCount: 0, last: 0, meta: null });
      }
      return stats.get(label);
    }
    function normalizeMeta(meta) {
      try {
        return typeof meta === 'function' ? meta() : (meta || null);
      } catch (e) {
        return { metaError: e && e.message ? e.message : String(e) };
      }
    }
    function record(label, duration, meta) {
      if (!AUTO_COLLECTION_ENABLED) return;
      const stat = getStat(label);
      stat.count += 1;
      stat.total += duration;
      stat.max = Math.max(stat.max, duration);
      stat.last = duration;
      if (duration >= LONG_TASK_MS) stat.longCount += 1;
      stat.meta = normalizeMeta(meta);
      const event = {
        at: Math.round(now()),
        label,
        duration: Number(duration.toFixed(2)),
        meta: stat.meta
      };
      events.push(event);
      if (duration >= LOG_TASK_MS) {
        console.log('[XDEX startup perf]', event);
      }
    }
    function mark(label, meta) {
      if (!AUTO_COLLECTION_ENABLED) return null;
      const event = {
        at: Math.round(now()),
        label: `mark:${label}`,
        duration: 0,
        meta: normalizeMeta(meta)
      };
      events.push(event);
      if (typeof performance !== 'undefined' && performance.mark) {
        try { performance.mark(`XDEX:${label}`); } catch (e) {}
      }
      return event;
    }
    // ★ 零开销快路径:关闭采集时直接转发调用,不读时钟、不构造 meta。
    // 热路径(滚动/观察器回调)上的埋点全部依赖这条早退,否则每帧两次 performance.now 会变成常驻成本。
    function begin() {
      return AUTO_COLLECTION_ENABLED ? now() : null;
    }
    // 与 begin 配对:token 为 null(未开启采集)时立即返回;
    // 用 `token == null` 而非 falsy 判断,避免 performance.now() 早期返回 0 被误判为「未计时」
    function finish(label, token, meta) {
      if (!AUTO_COLLECTION_ENABLED || token == null) return;
      record(label, now() - token, meta);
    }
    function measure(label, fn, meta) {
      if (!AUTO_COLLECTION_ENABLED) return fn();
      const started = now();
      try {
        return fn();
      } finally {
        record(label, now() - started, meta);
      }
    }
    async function measureAsync(label, fn, meta) {
      if (!AUTO_COLLECTION_ENABLED) return fn();
      const started = now();
      try {
        return await fn();
      } finally {
        record(label, now() - started, meta);
      }
    }
    function measureObserver(label, mutations, fn, meta) {
      if (!AUTO_COLLECTION_ENABLED) return fn();
      let result;
      const started = now();
      try {
        result = fn();
        return result;
      } finally {
        let added = 0;
        let removed = 0;
        for (const mutation of mutations || []) {
          added += mutation.addedNodes ? mutation.addedNodes.length : 0;
          removed += mutation.removedNodes ? mutation.removedNodes.length : 0;
        }
        record(`observer:${label}`, now() - started, () => Object.assign({
          records: mutations ? mutations.length : 0,
          added,
          removed
        }, normalizeMeta(meta) || {}));
      }
    }
    function summarizeRoot(root) {
      const scope = root && root.querySelectorAll ? root : document;
      if (!scope || !scope.querySelectorAll) return { root: 'unknown' };
      return {
        root: root === document ? 'document' : (root && root.className ? String(root.className).slice(0, 80) : root && root.nodeName),
        threads: scope.querySelectorAll('.h-threads-content').length,
        replies: scope.querySelectorAll('.h-threads-item-reply-main').length,
        imgs: scope.querySelectorAll('img').length,
        previewBoxes: scope.querySelectorAll('.h-preview-box').length
      };
    }
    function recordObserver(label, mutations, meta) {
      let added = 0;
      let removed = 0;
      for (const mutation of mutations || []) {
        added += mutation.addedNodes ? mutation.addedNodes.length : 0;
        removed += mutation.removedNodes ? mutation.removedNodes.length : 0;
      }
      record(`observer:${label}`, 0, () => Object.assign({
        records: mutations ? mutations.length : 0,
        added,
        removed
      }, normalizeMeta(meta) || {}));
    }
    function report(options = {}) {
      const table = Array.from(stats.values())
        .map(item => ({
          label: item.label,
          count: item.count,
          total: Number(item.total.toFixed(2)),
          max: Number(item.max.toFixed(2)),
          avg: Number((item.total / Math.max(1, item.count)).toFixed(2)),
          longCount: item.longCount,
          last: Number(item.last.toFixed(2)),
          meta: item.meta
        }))
        .sort((a, b) => b.total - a.total || b.max - a.max);
      const elapsed = Number((now() - startedAt).toFixed(2));
      if (options.table !== false) console.table(table);
      const result = {
        elapsed,
        autoWindowMs: AUTO_REPORT_AFTER_MS,
        stats: table,
        events: events.slice(-300),
        browserEvents: browserEvents.slice(-200)
      };
      console.log(options.auto ? '[XDEX startup perf auto report]' : '[XDEX startup perf report]', result);
      return result;
    }
    function reset() {
      stats.clear();
      events.length = 0;
      browserEvents.length = 0;
      console.log('[XDEX startup perf] reset');
      return 'reset';
    }
    // ==================== 场景记录与导出(真机测量用) ====================
    // 设计:每轮测量 = 一次 reset → 一次操作 → 一次 capture(name) 留档。
    // 留档只做「归档 + 精简」,不改变统计口径;导出的 JSON 用于跨版本/跨场景对照。
    const rounds = [];
    // 只保留判读所需的字段,避免导出体积随 events/browserEvents 膨胀
    const MEASURED_LABELS = [
      'hdLazy.onScrollFrame', 'hdLazy.measureQueue',                   // D:滚动帧内成本
      'seamless.runUserScrollFrame',                                    // D:无缝翻页判定
      'applyPageEnhancements.total', 'applyPageEnhancements.syncPreprocess', // A/B/C/D:增强收口
      'refresh.checkNext.syncDom',                                      // C:刷新按钮 / 末页旁路
      'refresh.postReply.syncDom',                                      // A:串内页发送后刷新
      // B:板块页/时间线发送后增量刷新(Step 5-8 的同步 DOM 工作与两处全文档调用)
      'boardQuickReply.total',
      'boardQuickReply.step5.measure', 'boardQuickReply.step6.insert', 'boardQuickReply.step7.compensate',
      'boardQuickReply.step8.post', 'boardQuickReply.buildNode',
      'boardQuickReply.deferred.refreshFilter', 'boardQuickReply.deferred.enablePostExpand', 'boardQuickReply.deferred.total',
      // B:await 分段(区分「网络耗时」与「桥接阻塞」——整链 7.5s 而同步 DOM 仅 ~28ms)
      'boardQuickReply.await.p1', 'boardQuickReply.await.tail', 'boardQuickReply.await.prev',
      'refreshFilterDisplay', 'applyFilters',                           // 过滤(作用域是否放大)
      'seamless.loadNext.total', 'seamless.loadNext.applyPageEnhancements',
      'seamless.loadNextBoard.total', 'seamless.loadNextBoard.applyPageEnhancements',
      'enableHDImageAndLayoutFix', 'enableHDImageAndLayoutFix.pending',
      'highlightPO', 'initContent'
    ];
    function collectScenario(round) {
      const observers = [];
      let observerTotal = 0;
      round.stats.forEach(s => {
        if (s.label.indexOf('observer.create:') === 0) {
          observers.push({ name: s.label.slice('observer.create:'.length), count: s.count });
          observerTotal += s.count;
        }
      });
      const frames = round.browserEvents.filter(e => e.label === 'browser:long-animation-frame');
      const blocking = frames.reduce((a, e) => a + (e.blockingDuration || 0), 0);
      // ★ 长帧归因汇总:按脚本来源聚合 duration 与 forcedStyleAndLayout,
      // 定位「未归因阻塞」属于哪个脚本/函数(LoAF scripts[] 提供)
      const scriptAgg = new Map();
      frames.forEach(f => (f.scripts || []).forEach(s => {
        const key = `${s.src || '?'}::${s.fn || s.invoker || '?'}`;
        const acc = scriptAgg.get(key) || { src: s.src, fn: s.fn, invoker: s.invoker, duration: 0, forcedStyleAndLayout: 0, times: 0 };
        acc.duration += s.duration || 0;
        acc.forcedStyleAndLayout += s.forcedStyleAndLayout || 0;
        acc.times += 1;
        scriptAgg.set(key, acc);
      }));
      const topScripts = Array.from(scriptAgg.values())
        .map(s => ({ ...s, duration: Number(s.duration.toFixed(2)), forcedStyleAndLayout: Number(s.forcedStyleAndLayout.toFixed(2)) }))
        .sort((a, b) => (b.duration + b.forcedStyleAndLayout) - (a.duration + a.forcedStyleAndLayout))
        .slice(0, 12);
      return {
        // ★ 关键字段前置:控制台复制时容易从尾部截断,而 blocking/LoAF 归因是排障的核心依据
        scenario: round.scenario,
        blockingDurationMs: Number(blocking.toFixed(2)),
        longAnimationFrames: frames.length,
        longTasks: round.browserEvents.filter(e => e.label === 'browser:longtask').length,
        topLoafScripts: topScripts,
        observerCreates: observers,
        observerCreateTotal: observerTotal,
        at: new Date(round.at).toISOString(),
        version: round.version,
        page: round.page,
        elapsedMs: round.elapsed,
        measured: round.stats.filter(s => MEASURED_LABELS.indexOf(s.label) !== -1),
        allLabels: round.stats.map(s => ({ label: s.label, count: s.count, total: s.total, max: s.max }))
      };
    }
    // capture(name):把当前累计统计按场景名留档并清空,返回该场景的精简摘要
    function capture(name) {
      if (!name) {
        console.warn('[XDEX perf] capture(name) 需要场景名,例如 capture("D-scroll")');
        return null;
      }
      const round = {
        scenario: name,
        at: Date.now(),
        version: (typeof VERSION !== 'undefined' ? VERSION : 'unknown'),
        page: location.pathname + location.search,
        stats: report({ table: false }).stats,
        elapsed: Number((now() - startedAt).toFixed(2)),
        browserEvents: browserEvents.slice()
      };
      const scenario = collectScenario(round);
      rounds.push(scenario);
      reset();
      console.log(`[XDEX perf] 已留档「${name}」:${scenario.measured.length} 个测量标签,`
        + `${scenario.observerCreateTotal} 个观察器创建,${scenario.longAnimationFrames} 个长帧`);
      // ★ 单行摘要:控制台复制大对象时常被截断,这里把判读所需的三个数压缩成一行
      const hot = scenario.measured.slice(0, 3)
        .map(m => `${m.label.replace(/^boardQuickReply\./, '')}=${m.total}`).join(' ');
      console.log(`[XDEX perf] 摘要 blocking=${scenario.blockingDurationMs}ms 长帧=${scenario.longAnimationFrames}`
        + ` 观察器=${scenario.observerCreateTotal} | ${hot}`);
      if (scenario.topLoafScripts.length) {
        console.log('[XDEX perf] 长帧归因 TOP(duration+forcedLayout,含强制布局):');
        console.table(scenario.topLoafScripts.map(s => ({
          src: s.src, fn: s.fn || s.invoker, times: s.times,
          duration: s.duration, forcedLayout: s.forcedStyleAndLayout
        })));
      }
      console.table(scenario.measured.map(m => ({ label: m.label, count: m.count, total: m.total, max: m.max })));
      return scenario;
    }
    // export():返回全部留档的 JSON 字符串(便于整段复制);dumps 保留挂到 window 供 DevTools copy()
    function exportAll() {
      const payload = {
        exportedAt: new Date().toISOString(),
        userAgent: (typeof navigator !== 'undefined' && navigator.userAgent) || '',
        viewport: (typeof window !== 'undefined') ? { w: window.innerWidth, h: window.innerHeight } : null,
        rounds
      };
      const json = JSON.stringify(payload, null, 2);
      try { window.__xdexPerfDump = json; } catch (e) {}
      return json;
    }
    // print():把 JSON 直接展开输出到控制台(剪贴板 API 需用户手势/受页面策略限制,不可靠,故不采用)。
    // 同时输出对象本体:DevTools 里可右键「Copy object」拿到结构化副本。
    function printAll() {
      const json = exportAll();
      console.log('[XDEX perf] 导出 JSON(%d bytes,可直接从下方选中复制):', json.length);
      console.log(json);
      console.log('[XDEX perf] 结构化对象(右键可 Copy object):');
      console.log(JSON.parse(json));
      return json;
    }
    function listRounds() {
      console.table(rounds.map(r => ({
        scenario: r.scenario,
        version: r.version,
        page: r.page,
        measured: r.measured.length,
        observers: r.observerCreateTotal,
        longFrames: r.longAnimationFrames,
        blockingMs: r.blockingDurationMs
      })));
      return rounds;
    }
    function clearRounds() {
      rounds.length = 0;
      console.log('[XDEX perf] 留档已清空');
      return 'cleared';
    }
    function installBrowserObservers() {
      if (typeof PerformanceObserver === 'undefined' || !PerformanceObserver.supportedEntryTypes) return;
      const supported = PerformanceObserver.supportedEntryTypes;
      if (supported.includes('longtask')) {
        try {
          const observer = new PerformanceObserver(list => {
            list.getEntries().forEach(entry => {
              const item = {
                at: Math.round(entry.startTime),
                label: 'browser:longtask',
                duration: Number(entry.duration.toFixed(2)),
                name: entry.name || '',
                attribution: Array.from(entry.attribution || []).map(attr => ({
                  name: attr.name || '',
                  containerType: attr.containerType || '',
                  containerName: attr.containerName || '',
                  containerSrc: attr.containerSrc || ''
                }))
              };
              browserEvents.push(item);
              console.log('[XDEX browser perf]', item);
            });
          });
          observer.observe({ entryTypes: ['longtask'] });
        } catch (e) {}
      }
      if (supported.includes('long-animation-frame')) {
        try {
          const observer = new PerformanceObserver(list => {
            list.getEntries().forEach(entry => {
              const item = {
                at: Math.round(entry.startTime),
                label: 'browser:long-animation-frame',
                duration: Number(entry.duration.toFixed(2)),
                renderStart: entry.renderStart ? Number(entry.renderStart.toFixed(2)) : 0,
                styleAndLayoutStart: entry.styleAndLayoutStart ? Number(entry.styleAndLayoutStart.toFixed(2)) : 0,
                blockingDuration: entry.blockingDuration ? Number(entry.blockingDuration.toFixed(2)) : 0,
                // ★ 归因:LoAF 的 scripts[] 给出每段脚本的 URL/入口函数与其中「强制样式与布局」耗时。
                // 590ms 级未归因阻塞靠它定位,比逐个加埋点快。
                scripts: Array.from(entry.scripts || []).map(s => ({
                  src: String(s.sourceURL || '').split('/').pop() || '(inline)',
                  fn: String(s.sourceFunctionName || ''),
                  invoker: String(s.invoker || ''),
                  duration: Number((s.duration || 0).toFixed(2)),
                  forcedStyleAndLayout: Number((s.forcedStyleAndLayoutDuration || 0).toFixed(2)),
                  pause: Number((s.pauseDuration || 0).toFixed(2))
                })).sort((a, b) => (b.duration + b.forcedStyleAndLayout) - (a.duration + a.forcedStyleAndLayout))
              };
              browserEvents.push(item);
              console.log('[XDEX browser perf]', item);
            });
          });
          observer.observe({ type: 'long-animation-frame', buffered: true });
        } catch (e) {}
      }
    }
    const api = { measure, measureAsync, measureObserver, begin, finish, record, recordObserver, mark, report, reset, summarizeRoot,
      capture, exportAll, printAll, listRounds, clearRounds };
    window.__xdexStartupPerfReport = report;
    window.__xdexStartupPerfReset = reset;
    target.__xdexStartupPerfReport = report;
    target.__xdexStartupPerfReset = reset;
    // 真机测量入口:capture(场景名) 留档 / listRounds() 查看 / print() 展开输出
    window.__xdexPerfCapture = capture;
    window.__xdexPerfRounds = listRounds;
    window.__xdexPerfExport = exportAll;
    window.__xdexPerfPrint = printAll;
    window.__xdexPerfClear = clearRounds;
    target.__xdexPerfCapture = capture;
    target.__xdexPerfRounds = listRounds;
    target.__xdexPerfExport = exportAll;
    target.__xdexPerfPrint = printAll;
    target.__xdexPerfClear = clearRounds;
    if (AUTO_COLLECTION_ENABLED) {
      installBrowserObservers();
      setTimeout(() => {
        report({ auto: true });
      }, AUTO_REPORT_AFTER_MS);
      console.log('[XDEX startup perf] auto collection started; auto report in 60s');
    }
    return api;
  })();
  function deferStartupTask(task, delay = 0, label = 'anonymous') {
    const run = () => {
      try {
        startupPerfDebug.measure(`deferStartupTask:${label}`, task, { delay });
      } catch (e) { console.warn('启动延迟任务失败', e); }
    };
    const schedule = () => setTimeout(run, delay);
    if (typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(schedule);
    } else {
      schedule();
    }
  }
  function deferStartupSteps(steps, delay = 0, label = 'anonymous') {
    const list = Array.isArray(steps) ? steps.filter(step => step && typeof step.run === 'function') : [];
    if (!list.length) return;
    const runStep = (index) => {
      if (index >= list.length) return;
      const step = list[index];
      startupPerfDebug.mark(`deferStartupSteps:${label}:${index + 1}:start`, { delay, index, step: step.label || '' });
      try {
        startupPerfDebug.measure(step.label || `deferStartupSteps:${label}:${index + 1}`, step.run, step.meta || { delay, index });
      } catch (e) { console.warn('启动分片任务失败', step.label || label, e); }
      finally {
        startupPerfDebug.mark(`deferStartupSteps:${label}:${index + 1}:end`, { delay, index, step: step.label || '' });
      }
      setTimeout(() => runStep(index + 1), 0);
    };
    const run = () => runStep(0);
    const schedule = () => setTimeout(run, delay);
    if (typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(schedule);
    } else {
      schedule();
    }
  }
  function addLastPageNumber(){
    document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination a').forEach(a => {
      if (a.textContent.trim() === '末页') {
        const m = a.href.match(/page=(\d+)/);
        if (m && !a.textContent.includes(`(${m[1]})`)) {
          a.textContent = `末页(${m[1]})`;
        }
      }
    });
  }
  // 自动监听 DOM 变化
  function observePagination(){
    if (observePagination.__bound) return;
    const observeTarget = document.body || document.documentElement;
    if (!observeTarget) {
      setTimeout(observePagination, 25);
      return;
    }
    observePagination.__bound = true;
    const observer = new MutationObserver(mutations => {
      let foundPagination = false;
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== 1) continue; // 只处理元素节点
          // 如果新增的是分页条本身,或它的子元素
          if (
            node.matches?.('ul.uk-pagination.uk-pagination-left.h-pagination') ||
            node.closest?.('ul.uk-pagination.uk-pagination-left.h-pagination')
          ) {
            foundPagination = true;
            break;
          }
        }
        if (foundPagination) break; // 已找到就不再继续遍历
      }
      if (foundPagination) {
        addLastPageNumber();
      }
    });
    observer.observe(observeTarget, {
      childList: true,
      subtree: true
    });
  }
  // 初始化
  //duplicatePagination();
  observePagination();

  /* --------------------------------------------------
   * tag 7. 自动+手动无缝翻页
   * -------------------------------------------------- */
  // ========== 公共增强函数 ==========
  // root: 新插入或替换的 DOM 节点(例如 repliesClone 或 targetReplies)
  // cfg: 当前配置对象
  function getPageEnhancementConfig(cfg) {
    try {
      // 关键:以 GM 最新配置为最高优先级,避免旧 cfg 覆盖新设置
      return Object.assign({}, SettingPanel.defaults, cfg || {}, GM_getValue(SettingPanel.key, {}));
    } catch (e) {
      return Object.assign({}, SettingPanel.defaults, cfg || {});
    }
  }
  // 插入 DOM 前可安全执行的同步增强:避免用户看到原始 DOM → 增强后 DOM 的闪烁过程
  function preprocessPageEnhancementsBeforeInsert(root, cfg) {
    const liveCfg = getPageEnhancementConfig(cfg);
    try { if (typeof runCriticalVisualEnhancements === 'function') runCriticalVisualEnhancements(root, liveCfg); } catch (e) {}
    try { if (liveCfg && liveCfg.enableAutoUrlLinkify && typeof runAutoUrlLinkify === 'function') runAutoUrlLinkify(root); } catch (e) {}
    try { if (liveCfg && liveCfg.extendQuote && typeof extendQuote === 'function') extendQuote(root); } catch (e) {}
  }
  function applyPageEnhancements(root, cfg, hotNodes) {
    const getLatestCfg = () => getPageEnhancementConfig(cfg);
    let liveCfg = getLatestCfg();
    // ★ 同步段埋点:插入前的预增强(URL 链接化 / 引用扩展)属同步成本,与下面的异步段分开计量
    startupPerfDebug.measure('applyPageEnhancements.syncPreprocess', () => preprocessPageEnhancementsBeforeInsert(root, liveCfg), { root: root === document ? 'document' : 'scoped' });
    setTimeout(() => {
      // ★ 埋点覆盖全部 4 个调用点(板块增量刷新 / 快速回复 / 无缝翻页两种):
      // 本函数体是 setTimeout 异步,外层 measure 只能量到同步的 preprocess 部分(数十微秒),
      // 真正耗时在这段回调里,故计时点必须放在回调内部。
      const __perfToken = startupPerfDebug.begin();
      liveCfg = getLatestCfg();
      try { if (typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail($(root)); } catch (e) {}
      // 只处理本次新增内容(root=新克隆/目标回复区),避免无缝翻页后对全页既有内容重复编号与重标 Po
      try { if (typeof highlightPO === 'function') highlightPO(root); } catch (e) {}
      // ★ HD 两函数是布局读写热点(handleGeneralElements 对每元素 读布局+写样式 交错强制重排)。
      // root 为「含旧回复的容器」(发送后/手动刷新路径)时必须限定本次新增节点(hotNodes),
      // 否则旧回复全部重跑 → 长串数百次强制重排 = 插入后卡顿主源。
      // root 为纯新内容(无缝翻页克隆)时 hotNodes 为空 → 保持原行为跑 root。
      if (Array.isArray(hotNodes) && hotNodes.length) {
        for (const hot of hotNodes) {
          const hotScope = hot.closest && hot.closest('.h-threads-item-reply') || hot;
          try { if (liveCfg && liveCfg.enableHDImageAndLayoutFix && typeof enableHDImageAndLayoutFix === 'function') enableHDImageAndLayoutFix(hotScope); } catch (e) {}
          try { if (liveCfg && liveCfg.enableHDImage && typeof enableHDImage === 'function') enableHDImage(hotScope); } catch (e) {}
        }
      } else {
        try { if (liveCfg && liveCfg.enableHDImageAndLayoutFix && typeof enableHDImageAndLayoutFix === 'function') enableHDImageAndLayoutFix(root); } catch (e) {}
        try { if (liveCfg && liveCfg.enableHDImage && typeof enableHDImage === 'function') enableHDImage(root); } catch (e) {}
      }
      try {
        if (typeof applyImageHideMode === 'function') {
          if (liveCfg && liveCfg.enableImageHideMode) {
            applyImageHideMode(liveCfg.applyImageHideMode || 'default', root);
          } else {
            applyImageHideMode('default', root);
          }
        }
      } catch (e) {}
      try { if (liveCfg && liveCfg.enableLinkBlank && typeof runLinkBlank === 'function') runLinkBlank(root); } catch (e) {}
      try { if (liveCfg && liveCfg.enableAutoUrlLinkify && typeof runAutoUrlLinkify === 'function') runAutoUrlLinkify(root); } catch (e) {}
      try { if (liveCfg && liveCfg.extendQuote && typeof extendQuote === 'function') extendQuote(root); } catch (e) {}
      try { if (liveCfg && liveCfg.enableQuotePreview && typeof enableQuotePreview === 'function') enableQuotePreview(); } catch (e) {}
      // 作用域必须与上方各步骤一致传 root:传 document 会在每次局部刷新/快速回复/翻页时
      // 对整条串的全部回复重跑「重置PO布局 → 过滤 → 重算PO布局」,成本随串长度线性放大
      try { refreshFilterDisplay(liveCfg, root); } catch (e) {}
      try { if (liveCfg && liveCfg.enableRelativeTime && typeof formatDateStrOnPage === 'function') formatDateStrOnPage(root); } catch (e) {}
      try { if (typeof initContent === 'function') initContent(root); } catch (e) {}
      //try { if (typeof autoHideRefView === 'function') autoHideRefView(root); } catch (e) {}
      try { if (typeof enablePostExpand === 'function') enablePostExpand(root); } catch (e) {}
      // 板块页/时间线串卡片「获取最新回复」按钮:与 enablePostExpand 同为逐卡片注入,必须随每次追加重应用
      // (原先只有启动时两处一次性 document 扫描,导致无缝翻页新增卡片无按钮)
      // 置于 enablePostExpand 之后,使 .js-toggle-mode 已存在、本按钮可正确排到其左侧
      try { if (typeof bindBoardThreadRefreshButtons === 'function') bindBoardThreadRefreshButtons(root); } catch (e) {}
      // if (typeof preventContentOverflow === 'function') {
      //   try { preventContentOverflow(document); } catch (e) {}
      // }
      // ★ meta 带 root 标识与回复数:用于判断成本是否随串长/DOM 规模放大(R2/R3/R4 的核心指标)
      startupPerfDebug.finish('applyPageEnhancements.total', __perfToken, () => {
        const el = root && root.nodeType === 1 ? root : null;
        return {
          root: el ? (el.className ? String(el.className).slice(0, 60) : el.nodeName) : 'none',
          replies: el && el.querySelectorAll ? el.querySelectorAll('.h-threads-item-reply-main').length : -1,
          doc: root === document
        };
      });
    }, 50);
  }
  function isEnhanceIslandAutoTitlePage() {
    return PageType.isThreadPage();
  }
  function refreshEnhanceIslandAutoTitle() {
    if (!isEnhanceIslandAutoTitlePage()) return;
    try {
      if (typeof window.enhanceIslandAutoTitle === 'function') {
        window.enhanceIslandAutoTitle();
      }
    } catch (e) {
      console.warn('[enhanceIslandAutoTitle] refresh failed:', e);
    }
  }
  // ========== 无缝翻页 / 发帖刷新共用的无状态工具层(Phase1:从 initSeamlessPaging 闭包抽出) ==========
  // 不依赖 loading/loadedPages 等状态机变量;签名与选择器保持与旧内联实现一致。
  // 调试 log 默认关闭:localStorage.xdexSeamlessDebug = '1' 或 window.__XDEX_SEAMLESS_DEBUG = true
  function isSeamlessDebugEnabled() {
    try {
      if (typeof window !== 'undefined' && window.__XDEX_SEAMLESS_DEBUG) return true;
      if (typeof localStorage !== 'undefined' && localStorage.getItem('xdexSeamlessDebug') === '1') return true;
    } catch (e) {}
    return false;
  }
  function seamlessDebugLog() {
    if (!isSeamlessDebugEnabled()) return;
    try { console.log.apply(console, arguments); } catch (e) {}
  }
  function getRealThreadsList(root = document) {
    const lists = Array.from((root || document).querySelectorAll('.h-threads-list'));
    return lists.find(el => !el.closest('.h-preview-box')) || null;
  }
  // 获取 DOM 中最大的 data-cloned-page(已被无缝加载进来的最大页)
  function getMaxClonedPageInDOM() {
    let max = 0;
    document.querySelectorAll('.h-threads-item-replies[data-cloned-page]').forEach(el => {
      const n = parseInt(el.getAttribute('data-cloned-page'), 10);
      if (!isNaN(n) && n > max) max = n;
    });
    return max;
  }
  function removeIdsFromNode(node) {
    if (!node || node.querySelectorAll === undefined) return;
    node.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
    if (node.hasAttribute && node.hasAttribute('id')) node.removeAttribute('id');
  }
  function parseLastPageFromPagination(pagUl) {
    if (!pagUl) return null;
    const anchors = Array.from(pagUl.querySelectorAll('a, span')).map(a => a.href || a.getAttribute('href') || '');
    const pageNums = anchors.map(h => {
      if (typeof parsePaginationPageNum === 'function') return parsePaginationPageNum(h);
      const m = (h || '').match(/[?&]page=(\d+)|\/page\/(\d+)\.html/);
      return m ? Number(m[1] || m[2]) : null;
    }).filter(n => !!n);
    if (pageNums.length === 0) return null;
    return Math.max(...pageNums);
  }
  function getDomLastPageNum() {
    const allPaginations = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
    if (allPaginations.length === 0) return null;
    const lastPagination = allPaginations[allPaginations.length - 1];
    return parseLastPageFromPagination(lastPagination);
  }
  function buildThreadPageUrl(threadId, pageNum) {
    // 如果当前是 /Forum/po/id/{threadId}/page/N.html 形式
    if (PageType.isPOThreadPage()) {
      return `${location.origin}/Forum/po/id/${threadId}/page/${pageNum}.html`;
    }
    // 默认 /t/{threadId}?page=N
    return `${location.origin}/t/${threadId}?page=${pageNum}`;
  }
  function extractFromHTML(htmlText) {
    const doc = new DOMParser().parseFromString(htmlText, 'text/html');
    const repliesAll = doc.querySelectorAll('.h-threads-item-replies');
    const replies = repliesAll.length ? repliesAll[0] : doc.querySelector('.h-threads-item-replies');
    let pagination = doc.querySelector('ul.uk-pagination.uk-pagination-left.h-pagination') ||
                    doc.querySelector('ul.uk-pagination.uk-pagination-left') ||
                    doc.querySelector('ul.uk-pagination');
    return { replies, pagination, doc };
  }
  // ========== Phase2:串内增量刷新共享核(无缝侧/发帖侧共用) ==========
  // 只承载:定目标页 / 建回复容器 / 按 id 增量 append / 同步底部分页。
  // 收尾判定(last/hasNext vs wasLastPage 涨页)仍由两边包装负责。
  function resolveThreadRefreshTargetPage(maxPage, maxCloned, pageCursor) {
    if ((maxPage && maxCloned === maxPage && maxCloned > 0) || (!maxPage && maxCloned > 0)) {
      return { kind: 'cloned', targetPage: maxCloned };
    }
    if (maxPage && pageCursor === maxPage && maxCloned === 0) {
      return { kind: 'main', targetPage: null };
    }
    return { kind: 'skip', targetPage: null };
  }
  function ensureThreadRepliesContainer(list, clonedPage) {
    const pageKey = clonedPage && clonedPage > 0 ? clonedPage : 0;
    let targetReplies = pageKey > 0
      ? list.querySelector('.h-threads-item-replies[data-cloned-page="' + pageKey + '"]')
      : list.querySelector('.h-threads-item-replies:not([data-cloned-page])');
    if (targetReplies) return { targetReplies: targetReplies, created: false };
    const threadItem = list.querySelector('.h-threads-item');
    if (!threadItem) return { targetReplies: null, created: false };
    targetReplies = document.createElement('div');
    targetReplies.className = 'h-threads-item-replies';
    if (pageKey > 0) targetReplies.setAttribute('data-cloned-page', String(pageKey));
    threadItem.appendChild(targetReplies);
    return { targetReplies: targetReplies, created: true };
  }
  function stripSystemTipReplies(root) {
    if (!root || !root.querySelectorAll) return;
    try {
      root.querySelectorAll('.h-threads-item-reply[data-threads-id="9999999"]').forEach(function (n) { n.remove(); });
    } catch (e) {}
  }
  // 按 data-threads-id 增量 append;返回 hasUpdate 与新节点列表
  function appendMissingRepliesByThreadsId(targetReplies, sourceRoot, options) {
    options = options || {};
    const replyOnly = options.replyOnly === true;
    const excludeSystemOnOld = options.excludeSystemOnOld === true;
    const excludeSystemOnNew = options.excludeSystemOnNew !== false;
    const sel = replyOnly ? '.h-threads-item-reply[data-threads-id]' : '[data-threads-id]';
    const oldItems = Array.from(targetReplies.querySelectorAll(sel));
    const oldIdSet = new Set();
    oldItems.forEach(function (item) {
      const id = item.getAttribute('data-threads-id') || (item.dataset && item.dataset.threadsId);
      if (!id) return;
      if (excludeSystemOnOld && id === '9999999') return;
      oldIdSet.add(id);
    });
    let newItems = Array.from(sourceRoot.querySelectorAll(sel));
    if (excludeSystemOnNew) {
      newItems = newItems.filter(function (item) {
        const id = item.getAttribute('data-threads-id') || (item.dataset && item.dataset.threadsId);
        return id && id !== '9999999';
      });
    }
    const appendedNodes = [];
    let hasUpdate = false;
    for (let i = 0; i < newItems.length; i++) {
      const item = newItems[i];
      const tid = item.getAttribute('data-threads-id') || (item.dataset && item.dataset.threadsId);
      if (!tid || oldIdSet.has(tid)) continue;
      hasUpdate = true;
      const node = item.cloneNode(true);
      targetReplies.appendChild(node);
      appendedNodes.push(node);
      oldIdSet.add(tid);
    }
    return { hasUpdate: hasUpdate, appendedNodes: appendedNodes };
  }
  function prepareAndSyncBottomPagination(doc) {
    const newPags = doc.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
    const newPag = newPags.length ? newPags[newPags.length - 1] : null;
    const oldPags = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
    const oldPag = oldPags.length ? oldPags[oldPags.length - 1] : null;
    if (newPag && oldPag) {
      const preparedPag = newPag.cloneNode(true);
      try { if (typeof rebuildPaginationPages === 'function') rebuildPaginationPages(preparedPag); } catch (e) {}
      try { if (typeof processPagination === 'function') processPagination(preparedPag); } catch (e) {}
      try { oldPag.innerHTML = preparedPag.innerHTML; } catch (e) { oldPag.replaceWith(preparedPag); }
    }
    return { newPag: newPag, oldPag: oldPag };
  }
  // ========== /Phase2 增量刷新共享核 ==========
  // ========== /无状态工具层 ==========
  function initSeamlessPaging() {
    let lastCheckAt = 0;
    // 所有需要被 window.SeamlessPaging 访问的变量都在此声明
    let loading = false;
    let forceLoadPending = false; // 手动重载 50ms 定时器待执行标记,防网络慢时重复触发
    let done = false;
    let loadedPages = new Set();
    let reachedLastPageAt = -1;
    let lastFinalToastTs = 0;
    let lastLoadedPage = 1;
    let observer = null;
    let observerFrozen = false;
    let hasUserInteracted = false;
    let lastUserScrollDir = 0;
    let lastScrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
    // Phase1:发现新页后的状态回退唯一入口(刷新按钮 / 末页 hasNext / SeamlessPaging.loadNext 共用)
    // 语义保持:delete 目标页、loading=false、可选回退 lastLoadedPage、lastCheckAt=0
    function prepareForceLoadNext(nextPage) {
      if (nextPage != null && nextPage !== undefined) {
        loadedPages.delete(nextPage);
        lastLoadedPage = nextPage - 1;
      } else {
        loadedPages.delete(lastLoadedPage + 1);
      }
      loading = false;
      lastCheckAt = 0;
    }
    try {
      const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      if (!cfg.enableSeamlessPaging) return;
      // let loading = false;
      // let done = false;
      // const loadedPages = new Set();
      // let reachedLastPageAt = -1;     // 记录“最后一页已加载”的页码(例如 20)
      // let lastFinalToastTs = 0;       // 防抖:末页提示的时间戳,避免重复弹
      const isThreadPage = PageType.isThreadPage();
      const isBoardPage = PageType.isBoardOrTimelinePage();
      const originInfo = (function () {
        const threadMatch = PageType.getThreadId();
        return {
          origin: location.origin,
          threadId: threadMatch || (document.querySelector('[data-threads-id]')?.getAttribute('data-threads-id') || null),
          page: PageType.getPageNum()
        };
      })();
      // let lastLoadedPage = originInfo.page || 1;
      lastLoadedPage = originInfo.page || 1;
      loadedPages.add(lastLoadedPage);
      const SENTINEL_ID = 'hld_auto_page_sentinel';
      let sentinel = document.getElementById(SENTINEL_ID);
      // let observer = null;           // 新增:让 observer 可被其他函数控制
      // let observerFrozen = false;    // 新增:哨兵冻结标记(大图激活时用)
      // 交互状态检测
      // let hasUserInteracted = false;
      // let lastUserScrollDir = 0;
      // let lastScrollTop = window.pageYOffset || document.documentElement.scrollTop || 0;
      // Phase3-2:串内/板块自动观察器去重(滚动意图 / 视口判定 / observer 绑定)
      function isSeamlessNearBottom(marginPx) {
        const margin = (marginPx == null) ? 200 : marginPx;
        return (window.scrollY + window.innerHeight) >= (document.documentElement.scrollHeight - margin);
      }
      function isSeamlessElementInViewport(el) {
        if (!el) return false;
        const r = el.getBoundingClientRect();
        return (r.bottom > 0 && r.top < window.innerHeight);
      }
      function getSeamlessLastRepliesActiveImageBox() {
        const container = getRootRepliesContainer();
        return container && container.lastReplies
          ? container.lastReplies.querySelector('.h-threads-img-box.h-active')
          : null;
      }
      // 共用触发门:相交 + 非 loading + 用户向下滚动意图;extraGate===false 时额外拦截(板块 done)
      function shouldTriggerSeamlessAutoLoad(entry, extraGate) {
        if (!entry || !entry.isIntersecting || loading) return false;
        if (extraGate === false) return false;
        return !!(hasUserInteracted && lastUserScrollDir > 0);
      }
      function bindSeamlessIntersectionObserver(placeSentinel, onIntersect) {
        placeSentinel();
        if (!sentinel) return null;
        const obs = new IntersectionObserver((entries) => {
          entries.forEach(entry => {
            onIntersect(entry);
          });
        }, { root: null, rootMargin: '0px', threshold: 0.05 });
        obs.observe(sentinel);
        return obs;
      }
      // ★ 滚动高频:本回调每次都读 scrollTop/scrollHeight,未合并会让滚动期间每帧重复强制布局。
      // 与同文件 bindScrollListener 的写法对齐:一帧只处理一次,滚动中不重复读取布局。
      let userScrollFrameId = 0;
      function onUserScroll() {
        if (userScrollFrameId) return;
        if (typeof requestAnimationFrame !== 'function') { runUserScrollFrame(); return; }
        userScrollFrameId = requestAnimationFrame(() => { userScrollFrameId = 0; runUserScrollFrame(); });
      }
      function runUserScrollFrame() {
        const __perfToken = startupPerfDebug.begin();
        hasUserInteracted = true;
        const curTop = window.pageYOffset || document.documentElement.scrollTop || 0;
        lastUserScrollDir = (curTop > lastScrollTop) ? 1 : (curTop < lastScrollTop ? -1 : lastUserScrollDir);
        lastScrollTop = curTop;
        // 新增:若处于冻结状态,满足条件则解冻并恢复观察
        if (observerFrozen && sentinel) {
          const maxDomLast = getDomLastPageNum();
          const atLastPage = !!(maxDomLast && lastLoadedPage >= maxDomLast);
          const nearBottom = isSeamlessNearBottom();
          // 若不再有激活大图,或已接近底部(两者满足任一),恢复观察
          // 注意:此处仍用全局 querySelector(与旧逻辑一致,不限最后 replies)
          const activeInView = isSeamlessElementInViewport(document.querySelector('.h-threads-img-box.h-active'));
          if (!activeInView || (atLastPage && nearBottom)) {
            try { observer.observe(sentinel); } catch (e) {}
            observerFrozen = false;
          }
        }
        // ★ 热路径埋点:滚动帧内成本 + 触发次数(frozen 短路时耗时≈0,靠 count 识别)
        startupPerfDebug.finish('seamless.runUserScrollFrame', __perfToken, () => ({ frozen: !!observerFrozen }));
      }
      function onWheel(e) {
        hasUserInteracted = true;
        if (typeof e.deltaY === 'number') lastUserScrollDir = e.deltaY > 0 ? 1 : -1;
      }
      window.addEventListener('scroll', onUserScroll, { passive: true });
      window.addEventListener('wheel', onWheel, { passive: true });
      // ======== 新增:桥接器 + 自动重执行器 ========
      function reinitForNewContent(container) {
        try {
          // 1. 派发自定义事件
          document.dispatchEvent(new CustomEvent('SeamlessPageAppended', {
            detail: { container }
          }));
          // 2. 模拟 DOMContentLoaded(部分脚本只监听这个)
          document.dispatchEvent(new Event('DOMContentLoaded'));
          // 3. 重新执行 container 内的 <script> 标签
          container.querySelectorAll('script').forEach(oldScript => {
            const newScript = document.createElement('script');
            if (oldScript.src) {
              newScript.src = oldScript.src;
            } else {
              newScript.textContent = oldScript.textContent;
            }
            [...oldScript.attributes].forEach(attr => {
              newScript.setAttribute(attr.name, attr.value);
            });
            oldScript.replaceWith(newScript);
          });
        } catch (err) {
          console.warn('reinitForNewContent error:', err);
        }
      }
      // ============================================
      // 串内页容器
      function getRootRepliesContainer() {
        const root = document.querySelector('.h-threads-item.uk-clearfix[data-threads-id]') ||
                    document.querySelector('.h-threads-item.uk-clearfix') ||
                    document.querySelector('[data-threads-id]');
        if (!root) return null;
        const replies = root.querySelectorAll('.h-threads-item-replies');
        if (!replies || replies.length === 0) return null;
        return { root, lastReplies: replies[replies.length - 1] };
      }
      // 哨兵节点创建:串页/板块页共用,避免两处复制样式
      function ensureSentinelNode() {
        if (!sentinel) {
          sentinel = document.createElement('div');
          sentinel.id = SENTINEL_ID;
          sentinel.style.height = '1px';
          sentinel.style.width = '100%';
          sentinel.style.pointerEvents = 'none';
        }
        return sentinel;
      }
      function placeSentinelAfter(anchor) {
        if (!anchor || !anchor.parentNode) return;
        ensureSentinelNode();
        if (anchor.nextSibling !== sentinel) {
          anchor.parentNode.insertBefore(sentinel, anchor.nextSibling);
        }
      }
      function ensureSentinelPlaced() {
        const containers = getRootRepliesContainer();
        if (!containers) return;
        // 旧:内联创建 sentinel 节点
        // if (!sentinel) { ... }
        placeSentinelAfter(containers.lastReplies);
      }
      // 板块页容器
      function ensureSentinelPlacedBoard() {
        const lists = document.querySelectorAll('.h-threads-list');
        const lastList = lists[lists.length - 1];
        if (!lastList) return;
        // 旧:内联创建 sentinel 节点(与串页重复)
        // if (!sentinel) { ... }
        placeSentinelAfter(lastList);
      }
      // Phase3-4:串内/板块哨兵放置对称入口(mode 策略;不改变放置目标)
      function ensureSeamlessSentinel(mode) {
        if (mode === 'board') ensureSentinelPlacedBoard();
        else ensureSentinelPlaced(); // thread(默认)
      }
      // Phase1:以下无状态工具已上提到闭包外共用实现;旧内联定义保留注释,避免重复声明遮盖外层函数
      // function removeIdsFromNode(node) {
      //   if (!node || node.querySelectorAll === undefined) return;
      //   node.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
      //   if (node.hasAttribute && node.hasAttribute('id')) node.removeAttribute('id');
      // }
      // function parseLastPageFromPagination(pagUl) {
      //   if (!pagUl) return null;
      //   const anchors = Array.from(pagUl.querySelectorAll('a, span')).map(a => a.href || a.getAttribute('href') || '');
      //   const pageNums = anchors.map(h => {
      //     if (typeof parsePaginationPageNum === 'function') return parsePaginationPageNum(h);
      //     const m = (h || '').match(/[?&]page=(\d+)|\/page\/(\d+)\.html/);
      //     return m ? Number(m[1] || m[2]) : null;
      //   }).filter(n => !!n);
      //   if (pageNums.length === 0) return null;
      //   return Math.max(...pageNums);
      // }
      // function getDomLastPageNum() {
      //   const allPaginations = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
      //   if (allPaginations.length === 0) return null;
      //   const lastPagination = allPaginations[allPaginations.length - 1];
      //   return parseLastPageFromPagination(lastPagination);
      // }
      // function buildThreadPageUrl(threadId, pageNum) {
      //   // 如果当前是 /Forum/po/id/{threadId}/page/N.html 形式
      //   if (/^\/Forum\/po\/id\/\d+/.test(location.pathname)) {
      //     return `${location.origin}/Forum/po/id/${threadId}/page/${pageNum}.html`;
      //   }
      //   // 默认 /t/{threadId}?page=N
      //   return `${location.origin}/t/${threadId}?page=${pageNum}`;
      // }
      function computeNextUrl() {
        const tid = originInfo.threadId || document.querySelector('[data-threads-id]')?.getAttribute('data-threads-id');
        if (!tid) return null;
        return buildThreadPageUrl(tid, lastLoadedPage + 1);
      }
      // ========== 新增:无缝翻页内部专用的刷新 / 判定工具(放在 loadNext 之前) ==========
      // done 为当前认定的末页添加手动局部刷新按钮,以避免回复数太少,无法滚动触发局部刷新,检测到为末页时一直存在
      // 从 root 中获取第一个非预览的 .h-threads-list (避免误取预览区)
      // Phase1:getRealThreadsList / getMaxClonedPageInDOM 已上提到闭包外
      // function getRealThreadsList(root = document) {
      //   const lists = Array.from((root || document).querySelectorAll('.h-threads-list'));
      //   return lists.find(el => !el.closest('.h-preview-box')) || null;
      // }
      // // 获取 DOM 中最大的 data-cloned-page(已被无缝加载进来的最大页)
      // function getMaxClonedPageInDOM() {
      //   let max = 0;
      //   document.querySelectorAll('.h-threads-item-replies[data-cloned-page]').forEach(el => {
      //     const n = parseInt(el.getAttribute('data-cloned-page'), 10);
      //     if (!isNaN(n) && n > max) max = n;
      //   });
      //   return max;
      // }
      // 刷新目标回复区(主页面回复区 或 data-cloned-page = 最大的克隆页)并检查是否有下一页
      // done(result) 回调会收到 { status: 'last'|'hasNext'|'error', nextPage?: number }
      // Phase2: old body replaced by shared-core thin wrapper
      // 刷新目标回复区(主页面回复区 或 data-cloned-page = 最大的克隆页)并检查是否有下一页
      // done(result) 回调会收到 { status: 'last'|'hasNext'|'error', nextPage?: number }
      // Phase2:定区/增量/分页走共享核;末页判定与 toast 仍在本包装
      function refreshRepliesAndCheckNext(done, options = {}, refreshGeneration) {
        const showResultToast = options.showResultToast !== false;
        const suppressResultToastOnHasNext = options.suppressResultToastOnHasNext !== false;
        const activeGeneration = refreshGeneration == null ? beginRefreshStatus() : refreshGeneration;
        if (!isCurrentRefreshStatus(activeGeneration)) return;
        try {
          const domMaxPage = getDomLastPageNum();
          const maxCloned = getMaxClonedPageInDOM();
          // 旧:内联 targetPage 分支(已收敛到 resolveThreadRefreshTargetPage)
          const resolved = resolveThreadRefreshTargetPage(domMaxPage, maxCloned, lastLoadedPage);
          // seamless 旧行为:未命中 cloned/main 时 targetPage 仍为 null 并继续刷主区
          const targetPage = resolved.kind === 'cloned' ? resolved.targetPage : null;
          const list = getRealThreadsList(document);
          if (!list) {
            console.warn('[refreshReplies] 未找到 .h-threads-list');
            showRefreshStatus('刷新回复失败,该串可能已被删除', 0, 'error');
            return done && done({ status: "error" });
          }
          const pageAttr = maxCloned > 0 ? maxCloned : 0;
          const ensured = ensureThreadRepliesContainer(list, pageAttr);
          const targetReplies = ensured.targetReplies;
          if (!targetReplies) {
            console.warn('[refreshReplies] 未找到 .h-threads-item,无法创建回复区');
            toast('刷新回复失败,未找到串容器', 0, { type: 'error' });
            return done && done({ status: "error" });
          }
          if (ensured.created) {
            console.log('[refreshReplies] 已创建空的回复区容器');
          }
          let fetchUrl = location.href;
          try {
            if (typeof originInfo !== 'undefined' && originInfo && originInfo.threadId && targetPage) {
              fetchUrl = buildThreadPageUrl(originInfo.threadId, targetPage);
            } else if (targetPage) {
              const u = new URL(location.href, location.origin);
              u.searchParams.set('page', String(targetPage));
              fetchUrl = u.toString();
            }
          } catch (e) {}
          fetch(fetchUrl, { credentials: 'same-origin' })
            .then(res => res.text())
            .then(html => {
              const doc = new DOMParser().parseFromString(html, 'text/html');
              const newList = getRealThreadsList(doc);
              if (!newList) {
                console.warn('[refreshReplies] 抓取页面中未找到 .h-threads-list');
                showRefreshStatus('刷新回复失败,该串可能已被删除', 0, 'error');
                return done && done({ status: "error" });
              }
              const newReplies = newList.querySelector('.h-threads-item-replies');
              if (!newReplies) {
                console.warn('[refreshReplies] 抓取页面中未找到 .h-threads-item-replies');
                toast('刷新回复失败,页面无回复内容', 0, { type: 'error' });
                return done && done({ status: "error" });
              }
              stripSystemTipReplies(newReplies);
              try {
                if (typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail($(newReplies));
                if (typeof applyFilters === 'function') applyFilters(cfg, newReplies);
              } catch (e) {
                console.warn('预处理过滤失败', e);
              }
              // Phase2:增量 append 走共享核(对齐旧:不过滤 9999999 在比较集合中的行为差异,由 strip 先清 new 侧)
              // ★ 计时点:只覆盖「响应到达后的同步 DOM 处理」(merge → applyPageEnhancements 同步段 → 回复统计)。
              // 网络往返被有意排除——卡顿来自脚本同步工作,不是 fetch。
              const __perfToken = startupPerfDebug.begin();
              const merge = appendMissingRepliesByThreadsId(targetReplies, newReplies, {
                replyOnly: false,
                excludeSystemOnOld: false,
                excludeSystemOnNew: false
              });
              const hasUpdate = merge.hasUpdate;
              const pagSync = prepareAndSyncBottomPagination(doc);
              const newPag = pagSync.newPag;
              try { if (typeof reinitForNewContent === 'function') reinitForNewContent(targetReplies); } catch (e) {}
              applyPageEnhancements(targetReplies, cfg, merge.appendedNodes);
              const allReplies = Array.from(targetReplies.querySelectorAll('.h-threads-item-reply'));
              const userReplies = allReplies.filter(el => el.getAttribute('data-threads-id') !== '9999999');
              const userCount = userReplies.length;
              const parsedLastFromReturned = (function() {
                const pag = newPag || doc.querySelector('ul.uk-pagination.uk-pagination-left.h-pagination') || doc.querySelector('ul.uk-pagination');
                return pag ? parseLastPageFromPagination(pag) : null;
              })();
              if (__perfToken != null) startupPerfDebug.finish('refresh.checkNext.syncDom', __perfToken, () => ({
                replies: userCount,
                updated: !!hasUpdate,
                domPages: document.querySelectorAll('.h-threads-item-replies[data-cloned-page]').length
              }));
              if (userCount < 19) {
                const result = { status: 'last', hasUpdate };
                if (showResultToast && isCurrentRefreshStatus(activeGeneration)) showRefreshStatus(hasUpdate ? "已更新" : "无更新", hasUpdate ? 900 : 0, hasUpdate ? '' : 'hold');
                if (typeof done === 'function') done(result);
                addRefreshButtonIfNeeded();
                return;
              }
              if (parsedLastFromReturned && parsedLastFromReturned > lastLoadedPage) {
                const result = { status: 'hasNext', nextPage: lastLoadedPage + 1, hasUpdate };
                if (showResultToast && !suppressResultToastOnHasNext) toast(hasUpdate ? "已更新" : "无更新", hasUpdate ? 900 : 0, { queue: false, key: 'refresh-status', type: hasUpdate ? '' : 'hold' });
                if (typeof done === 'function') done(result);
                return;
              } else {
                const result = { status: 'last', hasUpdate };
                if (showResultToast && isCurrentRefreshStatus(activeGeneration)) showRefreshStatus(hasUpdate ? "已更新" : "无更新", hasUpdate ? 900 : 0, hasUpdate ? '' : 'hold');
                if (typeof done === 'function') done(result);
                addRefreshButtonIfNeeded();
                return;
              }
            })
            .catch(err => {
              console.error('refreshRepliesAndCheckNext error:', err);
          if (isCurrentRefreshStatus(activeGeneration)) showRefreshStatus('刷新回复区失败', 0, 'error');
          if (typeof done === 'function') done({ status: 'error' });
            });
        } catch (err) {
          console.error('refreshRepliesAndCheckNext pre error:', err);
          if (typeof done === 'function') done({ status: 'error' });
        }
      }
      // Phase1:extractFromHTML 已上提到闭包外共用实现
      // function extractFromHTML(htmlText) {
      //   const doc = new DOMParser().parseFromString(htmlText, 'text/html');
      //   const repliesAll = doc.querySelectorAll('.h-threads-item-replies');
      //   const replies = repliesAll.length ? repliesAll[0] : doc.querySelector('.h-threads-item-replies');
      //   let pagination = doc.querySelector('ul.uk-pagination.uk-pagination-left.h-pagination') ||
      //                   doc.querySelector('ul.uk-pagination.uk-pagination-left') ||
      //                   doc.querySelector('ul.uk-pagination');
      //   return { replies, pagination, doc };
      // }
      // Phase3-3:刷新检查结果共用处理(刷新按钮 click / loadNext 末页旁路)
      // 语义:error 静默;hasNext → toast + prepareForceLoadNext + 延迟 loadNext;last → 已更新/无更新
      function handleSeamlessRefreshCheckResult(result, refreshGeneration) {
        if (!result || result.status === 'error') return;
        if (!isCurrentRefreshStatus(refreshGeneration)) return;
        if (result.status === 'hasNext' && result.nextPage) {
          showRefreshStatus(`发现新回复,正在加载第 ${result.nextPage} 页……`, 700);
          // 旧:内联状态回退(已统一到 prepareForceLoadNext)
          // loadedPages.delete(result.nextPage);
          // loading = false;
          // lastLoadedPage = result.nextPage - 1;
          // lastCheckAt = 0;
          prepareForceLoadNext(result.nextPage);
          setTimeout(() => loadNext(refreshGeneration), 50);
        } else if (result.status === 'last') {
          // 「无更新」用 hold:无红框但长驻(无操作 30s / 交互后 5s 渐隐),避免很快被刷掉;
          // 「已更新」仍是普通短提示给足阅读时间
          showRefreshStatus(result.hasUpdate ? '已更新' : '无更新', result.hasUpdate ? 1800 : 0, result.hasUpdate ? '' : 'hold'); // 结果提示给足阅读时间(原 900ms 偏短,易漏看后重复刷新)
        }
      }
      // Phase3-3:刷新按钮旁路(显示/overlay/分页监听与 loadNext 主路径分离)
      function getSeamlessBottomPagination() {
        const allPaginations = document.querySelectorAll('ul.uk-pagination');
        return allPaginations.length ? allPaginations[allPaginations.length - 1] : null;
      }
      function updateSeamlessRefreshBtnDisplay(btn, pag) {
        if (!btn) return;
        if (!pag) {
          btn.style.display = 'none';
          return;
        }
        const hasNext = !!pag.querySelector('li:last-child a');
        if (hasNext) {
          btn.style.display = 'none';
          return;
        }
        // 检查浮窗状态
        const overlay = document.querySelector('.qp-overlay');
        const overlayQuote = document.querySelector('.qp-overlay-quote');
        const overlayOpen = (overlay && overlay.style.display === 'block');
        const overlayQuoteOpen = (overlayQuote && overlayQuote.style.display === 'block');
        if (overlayOpen || overlayQuoteOpen) {
          btn.style.display = 'none';
        } else {
          // 显示时统一写 flex + 居中内联(覆盖任何 display:block 残留)
          btn.style.display = 'flex';
          btn.style.alignItems = 'center';
          btn.style.justifyContent = 'center';
        }
      }
      function ensureSeamlessRefreshButtonNode() {
        let btn = document.getElementById('seamless-refresh-btn');
        if (btn) return btn;
        btn = document.createElement('div');
        btn.id = 'seamless-refresh-btn';
        btn.className = 'qp-reset-btn seamless-refresh-btn xdex-icon-btn';
        btn.title = '手动检查回复更新';
        // btn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path style="fill:none" d="M18 18A8.5 8.5 0 1 1 18.5 6.5"/><path style="fill:none" d="M18.5 6.5l-1.3 3.6"/><path style="fill:none" d="M19.2 10.6L18.5 6.5l-3.2 2.7"/></svg>';
        btn.innerHTML = '<svg class="xdex-icon-sync" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 0 0-14.9-3"/><path d="M5.1 4v4h4"/><path d="M4 13a8 8 0 0 0 14.9 3"/><path d="M18.9 20v-4h-4"/></svg>';
        // --- 固定位置样式(外观走 xdex-icon-btn,内联仅定位与显隐) ---
        btn.style.position = 'fixed';
        btn.style.right = '12px';
        btn.style.bottom = '60px';
        // 层级置于拓展坞(.hld__docker z-index:9998)之上:串内页两者位置可能重叠;下行为 10000,勿再被覆盖
        btn.style.zIndex = '10000';
        btn.style.display = 'none';   // 默认不显示
        document.body.appendChild(btn);
        // 点击触发"局部刷新 → 若有下一页则无缝翻页"
        btn.addEventListener('click', () => {
          try {
            btn.classList.add('xdex-icon-loading');
            const refreshGeneration = beginRefreshStatus();
            showRefreshStatus("正在刷新……", 1500);
            // 旧:内联 hasNext/last 分支(已抽到 handleSeamlessRefreshCheckResult)
            refreshRepliesAndCheckNext(
              (result) => { btn.classList.remove('xdex-icon-loading'); handleSeamlessRefreshCheckResult(result, refreshGeneration); },
              { showResultToast: false },
              refreshGeneration
            );
          } catch (e) {
            btn.classList.remove('xdex-icon-loading');
            console.warn('刷新按钮触发失败:', e);
          }
        });
        return btn;
      }
      function observeSeamlessRefreshOverlays(btn) {
        const overlays = [document.querySelector('.qp-overlay'), document.querySelector('.qp-overlay-quote')];
        overlays.forEach(el => {
          if (!el) return;
          // 同一元素只挂一次:浮窗为常驻单例,重复调用不应叠加属性观察器;
          // 浮窗若为新建元素则无此标记,仍会被观察,保留「晚出现的浮窗也能被挂上」的行为
          if (el.__xdexSeamlessOverlayObserved) return;
          el.__xdexSeamlessOverlayObserved = true;
          // ★ 计数埋点(H1-H3 真机验收判据):实例数应恒定,不随开关浮窗次数增长
          startupPerfDebug.record('observer.create:overlay', 0, { tag: 'qp-overlay' });
          const obs = new MutationObserver(() => {
            updateSeamlessRefreshBtnDisplay(btn, getSeamlessBottomPagination());
          });
          obs.observe(el, { attributes: true, attributeFilter: ['style'] });
        });
        return btn;
      }
      // body 级与分页栏级观察器都提为共享单例:原实现每次调用都各自新建且从不 disconnect,
      // 数量随「刷新按钮点击 + 每次末页判定」线性累积,使每次 DOM 变更的固定开销成倍放大
      let seamlessGlobalObserver = null;
      let seamlessPagObserver = null;
      let seamlessPagObserverTarget = null;
      // 兼容旧调用名;内部完成按钮创建 + overlay/分页监听
      function addRefreshButtonIfNeeded() {
        // 旧:整段内联在 loadNext 旁(Phase3-3 拆为显示/监听辅助 + 共用结果处理)
        const btn = ensureSeamlessRefreshButtonNode();
        // --- 始终监听页面最底部的分页栏 ---
        function observeBottomPagination() {
          const pag = getSeamlessBottomPagination();
          if (!pag) return;
          // 先更新一次显示状态
          updateSeamlessRefreshBtnDisplay(btn, pag);
          // 同一分页栏复用现有观察器,避免每次 DOM 变更都 disconnect + 重建
          if (seamlessPagObserver && seamlessPagObserverTarget === pag) return;
          // 如果已有旧的 observer,先断开
          if (seamlessPagObserver) {
            seamlessPagObserver.disconnect();
          }
          // 新建 observer 监听底部分页栏的变化
          // ★ 计数埋点(H2 真机验收判据):同目标复用时不重建,调用多次也应为 1
          startupPerfDebug.record('observer.create:pag', 0, { tag: 'bottom-pagination' });
          seamlessPagObserver = new MutationObserver(() => {
            updateSeamlessRefreshBtnDisplay(btn, getSeamlessBottomPagination());
          });
          seamlessPagObserver.observe(pag, { childList: true, subtree: true });
          seamlessPagObserverTarget = pag;
        }
        // 初始绑定
        observeSeamlessRefreshOverlays(btn);
        // 初始监听一次
        observeBottomPagination();
        // 每次 DOM 可能插入新分页栏时,重新绑定监听(只挂一次,回调内复用同一分页栏观察器逻辑)
        if (!seamlessGlobalObserver) {
          // ★ 计数埋点(H1 真机验收判据):body 级观察器必须恒为 1,不得随翻页/刷新次数增长
          startupPerfDebug.record('observer.create:body', 0, { tag: 'document.body' });
          seamlessGlobalObserver = new MutationObserver(() => {
            observeBottomPagination();
          });
          seamlessGlobalObserver.observe(document.body, { childList: true, subtree: true });
        }
      }
      // 串内页加载
      async function loadNext(refreshGeneration) {
        const activeGeneration = refreshGeneration == null ? beginRefreshStatus() : refreshGeneration;
        if (!isCurrentRefreshStatus(activeGeneration)) return;
        const loadNextStarted = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
        startupPerfDebug.mark('seamless.loadNext.start', { lastLoadedPage, loading, done });
        seamlessDebugLog('[loadNext] 函数被调用');
        const now = Date.now();
        seamlessDebugLog('[loadNext] 检查防抖: now - lastCheckAt =', now - lastCheckAt);
        if (now - lastCheckAt < 1000) {
          seamlessDebugLog('[loadNext] 防抖拦截,返回');
          return;
        }
        lastCheckAt = now;
        seamlessDebugLog('[loadNext] 通过防抖检查');
         const domLast = getDomLastPageNum();
      // if (domLast && lastLoadedPage >= domLast) {
      //   return;
      // }
        // 新增:基于当前页底分页DOM的“新鲜判定”
        function checkPaginationState(lastLoadedPage) {
          const allPaginations = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
          const bottomPag = allPaginations.length ? allPaginations[allPaginations.length - 1] : null;
          if (!bottomPag) {
            return { status: 'noPagination', stop: false };
          }
          const lis = Array.from(bottomPag.querySelectorAll('li'));
          const numericLis = lis.filter(li => /^\d+$/.test(li.textContent.trim()));
          const hasNextLinkNow = lis.some(li => /下一页|下页|Next|›|»|→/i.test(li.textContent.trim()));
          // 只有一页
          if (numericLis.length <= 1) {
            return { status: 'singlePage', stop: true, message: '仅一页,无需翻页' };
          }
          // 已到末页
          const maxDomLast = getDomLastPageNum();
          if (maxDomLast && lastLoadedPage >= maxDomLast) {
            return { status: 'atLastPage', stop: true, message: '已经是最后一页了' };
          }
          // 没有“下一页”按钮且也没有更大页码
          if (!hasNextLinkNow && maxDomLast && lastLoadedPage + 1 > maxDomLast) {
            return { status: 'noNextLink', stop: true, message: '没有下一页' };
          }
          // 正常情况
          return { status: 'ok', stop: false };
        }
        // 调用新鲜判定
        const state = checkPaginationState(lastLoadedPage);
        if (state.stop) {
          if (state.message) {
            const now2 = Date.now();
            if (now2 - lastFinalToastTs > 3000) {
              toast('正在检查新回复……', 900, { queue: false, key: 'refresh-status' });
              lastFinalToastTs = now2;
            }
          }
          // 👉 每次末页判定时,都刷新最新回复区和分页
          // 旧:内联 hasNext/last 分支(Phase3-3 已抽到 handleSeamlessRefreshCheckResult)
          const refreshGeneration = beginRefreshStatus();
          refreshRepliesAndCheckNext(
            (result) => handleSeamlessRefreshCheckResult(result, refreshGeneration),
            { showResultToast: false },
            refreshGeneration
          );
          return;
        }
        // if (loading) return;
        // const nextPageNum = lastLoadedPage + 1;
        // if (loadedPages.has(nextPageNum)) return;
        seamlessDebugLog('[loadNext] loading =', loading);
        if (loading) {
          seamlessDebugLog('[loadNext] loading 为 true,返回');
          return;
        }
        const nextPageNum = lastLoadedPage + 1;
        seamlessDebugLog('[loadNext] 计算下一页页码: lastLoadedPage =', lastLoadedPage, ', nextPageNum =', nextPageNum);
        seamlessDebugLog('[loadNext] loadedPages 包含的页码:', Array.from(loadedPages));
        if (loadedPages.has(nextPageNum)) {
          seamlessDebugLog('[loadNext] nextPageNum 已在 loadedPages 中,返回');
          return;
        }
        seamlessDebugLog('[loadNext] 准备加载页码:', nextPageNum);
        const nextUrl = computeNextUrl();
        if (!nextUrl) { return; }
        showRefreshStatus(`正在加载第 ${nextPageNum} 页……`, 900);
        loading = true;
        try {
          const res = await startupPerfDebug.measureAsync('seamless.loadNext.fetch', () => fetch(nextUrl, { credentials: 'same-origin' }), { nextPageNum, nextUrl });
          if (!res.ok) {
              if (isCurrentRefreshStatus(activeGeneration)) showRefreshStatus('刷新失败,网络错误', 0, 'error');
              return;
          }
          const html = await startupPerfDebug.measureAsync('seamless.loadNext.responseText', () => res.text(), { nextPageNum });
          const { replies, pagination } = startupPerfDebug.measure('seamless.loadNext.extractFromHTML', () => extractFromHTML(html), { nextPageNum, htmlLength: html.length });
          if (!replies) { return; }
          let pagClone = startupPerfDebug.measure('seamless.loadNext.clonePagination', () => pagination ? pagination.cloneNode(true) : null, { nextPageNum });
          if (!pagClone) {
            pagClone = document.createElement('ul');
            pagClone.className = 'uk-pagination uk-pagination-left h-pagination';
          }
          pagClone.setAttribute('hld-auto-page', 'ok');
          removeIdsFromNode(pagClone);
          const lastPageNum = parseLastPageFromPagination(pagClone);
          if (lastPageNum) pagClone.setAttribute('data-last-page', String(lastPageNum));
          pagClone.setAttribute('data-cloned-page', String(nextPageNum));
          // 插入 DOM 前直接重建分页栏,避免先显示原版 3 页码再被 observer 二次修正造成闪烁/循环
          if (typeof rebuildPaginationPages === 'function') rebuildPaginationPages(pagClone);
          if (typeof processPagination === 'function') processPagination(pagClone);
          const repliesClone = startupPerfDebug.measure('seamless.loadNext.cloneReplies', () => replies.cloneNode(true), { nextPageNum, replies: replies.querySelectorAll ? replies.querySelectorAll('.h-threads-item-reply').length : 0 });
          repliesClone.setAttribute('data-cloned-page', String(nextPageNum));
          removeIdsFromNode(repliesClone);
          // 插入 DOM 前同步执行会影响视觉的公共增强,避免用户看到未处理态闪烁
          try { if (typeof preprocessPageEnhancementsBeforeInsert === 'function') preprocessPageEnhancementsBeforeInsert(repliesClone, cfg); } catch (e) {}
          const containers = getRootRepliesContainer();
          if (!containers) { return; }
          const { lastReplies } = containers;
          startupPerfDebug.measure('seamless.loadNext.insertDom', () => {
            lastReplies.insertAdjacentElement('afterend', pagClone);
            pagClone.insertAdjacentElement('afterend', repliesClone);
          }, { nextPageNum });
          // ======== 新增:让其他脚本对新内容生效 ========
          startupPerfDebug.measure('seamless.loadNext.reinitForNewContent', () => reinitForNewContent(repliesClone), { nextPageNum });
          startupPerfDebug.measure('seamless.loadNext.applyPageEnhancements', () => applyPageEnhancements(repliesClone, cfg), { nextPageNum });
          // ============================================
          // 更新底部分页条
          const allPaginations = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
          if (allPaginations.length > 0) {
            const lastPagination = allPaginations[allPaginations.length - 1];
            if (lastPagination && lastPagination !== pagClone) {
              lastPagination.replaceWith(pagClone.cloneNode(true));
            }
          }
          loadedPages.add(nextPageNum);
          lastLoadedPage = nextPageNum;
          try { history.pushState(null, '', nextUrl); } catch (e) {}
          recordThreadHistoryProgress({ url: nextUrl, page: nextPageNum, reason: 'seamless-page', touchVisitedAt: true });
          refreshEnhanceIslandAutoTitle();
          ensureSentinelPlaced();
          const hasNextLink = (() => {
              const anchorsText = Array.from(pagClone.querySelectorAll('a')).map(a => a.textContent.trim());
              if (anchorsText.some(t => /下一页|下页|Next|next|›|»|→/.test(t))) return true;
              const parsed = parseLastPageFromPagination(pagClone);
              if (parsed && parsed > nextPageNum) return true;
              return false;
            })();
            // 不在这里吐司“已经是最后一页了”,仅记录“末页已加载”的页码
            if (!hasNextLink) {
              reachedLastPageAt = nextPageNum;
            } else {
              // 仍有下一页可能,清除标记
              reachedLastPageAt = -1;
            }
        } catch (e) {
          console.warn('seamless paging loadNext error:', e);
          toast('加载失败,请稍后重试', 0, { type: 'error' });
          return;
        } finally {
          loading = false;
          const finished = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
          startupPerfDebug.record('seamless.loadNext.total', finished - loadNextStarted, { lastLoadedPage, loading, done });
          startupPerfDebug.mark('seamless.loadNext.end', { lastLoadedPage, loading, done });
        }
      }
      async function loadNextBoard() {
        const loadNextBoardStarted = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
        startupPerfDebug.mark('seamless.loadNextBoard.start', { lastLoadedPage, loading, done });
        if (loading || done) return;
        const nextPageNum = lastLoadedPage + 1;
        if (loadedPages.has(nextPageNum)) return;
        let nextUrl;
        if (PageType.isTimelinePage()) {
          // 时间线模式
          const base = location.pathname.replace(/\/page\/\d+\.html$/, ''); // 去掉已有的 /page/N.html
          nextUrl = `${location.origin}${base}/page/${nextPageNum}.html`;
        } else {
          // 默认板块模式
          nextUrl = `${location.origin}${location.pathname}?page=${nextPageNum}`;
        }
        loading = true;
        try {
          const res = await startupPerfDebug.measureAsync('seamless.loadNextBoard.fetch', () => fetch(nextUrl, { credentials: 'same-origin' }), { nextPageNum, nextUrl });
          if (!res.ok) { done = true; return; }
          const html = await startupPerfDebug.measureAsync('seamless.loadNextBoard.responseText', () => res.text(), { nextPageNum });
          const doc = startupPerfDebug.measure('seamless.loadNextBoard.parseHTML', () => new DOMParser().parseFromString(html, 'text/html'), { nextPageNum, htmlLength: html.length });
          const list = doc.querySelector('.h-threads-list');
          const pagination = doc.querySelector('ul.uk-pagination.uk-pagination-left.h-pagination');
          // 板块页空页面检查(速报2)
          if (!list) {
            done = true;
            const now = Date.now();
            if (now - lastFinalToastTs > 3000) {
              toast('已经是最后一页了');
              lastFinalToastTs = now;
            }
            return;
          }
          // 检查 list 是否为空(没有子元素或只有空白内容)
          const hasContent = list.children.length > 0 || list.textContent.trim().length > 0;
          if (!hasContent) {
            done = true;
            const now = Date.now();
            if (now - lastFinalToastTs > 3000) {
              toast('已经是最后一页了');
              lastFinalToastTs = now;
            }
            return;
          }
          toast(`正在加载第 ${nextPageNum} 页……`);
          if (!list) { done = true; return; }
          const listClone = startupPerfDebug.measure('seamless.loadNextBoard.cloneList', () => list.cloneNode(true), { nextPageNum, threads: list.querySelectorAll ? list.querySelectorAll('.h-threads-item').length : 0 });
          removeIdsFromNode(listClone);
          // 插入 DOM 前同步执行会影响视觉的公共增强,避免用户看到未处理态闪烁
          try { if (typeof preprocessPageEnhancementsBeforeInsert === 'function') preprocessPageEnhancementsBeforeInsert(listClone, cfg); } catch (e) {}
          // 找到当前页面最后一个 .h-threads-list
          const lists = document.querySelectorAll('.h-threads-list');
          const lastList = lists[lists.length - 1];
          if (lastList) {
            let pagClone = pagination ? pagination.cloneNode(true) : null;
            if (pagClone) {
              removeIdsFromNode(pagClone);
              // 插入 DOM 前直接重建分页栏,避免原版 3 页码闪烁后再修正
              if (typeof rebuildPaginationPages === 'function') rebuildPaginationPages(pagClone);
              if (typeof processPagination === 'function') processPagination(pagClone);
              startupPerfDebug.measure('seamless.loadNextBoard.insertDom', () => {
                lastList.insertAdjacentElement('afterend', pagClone);
                pagClone.insertAdjacentElement('afterend', listClone);
              }, { nextPageNum });
              // ======== 新增:让其他脚本对新内容生效 ========
              startupPerfDebug.measure('seamless.loadNextBoard.reinitForNewContent', () => reinitForNewContent(listClone), { nextPageNum });
              startupPerfDebug.measure('seamless.loadNextBoard.applyPageEnhancements', () => applyPageEnhancements(listClone, cfg), { nextPageNum });
              // ============================================
              // 更新底部分页条
              // ★ 测试版块页:守护放回的原生分页(页1高亮)插在列表1 之后、
              //   本函数又把 pagClone(页2) 插到列表1 之后 → 出现「页1 + 页2」两条相邻分页栏。
              //   修复:找出**除 titlePag 与 pagClone 外**的原生分页(守护放回的那条),
              //   replaceWith 前先打 xdexSeamlessReplaced 豁免标记——守护器见标记即放行,
              //   否则 replaceWith 的移除会触发放回死循环。
              const allPaginations = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
              for (const oldPag of allPaginations) {
                if (oldPag === pagClone) continue;
                if (oldPag.dataset && oldPag.dataset.xdexTitleClone === '1') continue; // 页首页码不动
                oldPag.dataset.xdexSeamlessReplaced = '1';
                oldPag.replaceWith(pagClone.cloneNode(true));
              }
            } else {
              startupPerfDebug.measure('seamless.loadNextBoard.insertDom', () => lastList.insertAdjacentElement('afterend', listClone), { nextPageNum });
              // ======== 新增:让其他脚本对新内容生效 ========
              startupPerfDebug.measure('seamless.loadNextBoard.reinitForNewContent', () => reinitForNewContent(listClone), { nextPageNum });
              try { if (cfg.enableQuotePreview && typeof enableQuotePreview === 'function') startupPerfDebug.measure('seamless.loadNextBoard.enableQuotePreview', () => enableQuotePreview(), { nextPageNum }); } catch (e) {}
              if (typeof initContent === 'function') {
                startupPerfDebug.measure('seamless.loadNextBoard.initContent', () => initContent(listClone), { nextPageNum });   // 重新绑定引用悬浮预览
              }
              //autoHideRefView(listClone); // 拓展引用悬浮
              // ============================================
            }
          }
        loadedPages.add(nextPageNum);
        lastLoadedPage = nextPageNum;
        try { history.pushState(null, '', nextUrl); } catch (e) {}
        recordThreadHistoryProgress({ url: nextUrl, page: nextPageNum, reason: 'seamless-page', touchVisitedAt: true });
        refreshEnhanceIslandAutoTitle();
        const hasNextLink = pagination && Array.from(pagination.querySelectorAll('a')).some(a => /下一页|下页|Next|›|»|→/i.test(a.textContent));
        //if (!hasNextLink) done = true;
        } catch (e) {
          console.warn('board paging loadNext error:', e);
        return;
        } finally {
          loading = false;
          const finished = typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();
          startupPerfDebug.record('seamless.loadNextBoard.total', finished - loadNextBoardStarted, { lastLoadedPage, loading, done });
          startupPerfDebug.mark('seamless.loadNextBoard.end', { lastLoadedPage, loading, done });
        }
      }
      // 新增:检查激活大图并按需冻结/解冻观察器(限定在当前串的最后一个 replies 容器)
      function checkActiveImageAndToggleObserver() {
        // 旧:内联 nearBottom / activeInView 计算(已抽到 Phase3-2 共用)
        const activeBox = getSeamlessLastRepliesActiveImageBox();
        const hasActive = !!activeBox;
        const maxDomLast = getDomLastPageNum();
        const atLastPage = !!(maxDomLast && lastLoadedPage >= maxDomLast);
        const nearBottom = isSeamlessNearBottom();
        const activeInView = isSeamlessElementInViewport(activeBox);
        // 末页 + 激活在视口内 + 未接近底部 → 冻结(disconnect)
        if (atLastPage && activeInView && !nearBottom) {
          try { observer && observer.disconnect(); } catch (e) {}
          observerFrozen = true;
          return;
        }
        // 其他情况 → 恢复观察(如果之前被冻结)
        if (observerFrozen && sentinel && observer) {
          try { observer.observe(sentinel); } catch (e) {}
          observerFrozen = false;
        }
      }
      // 新增:绑定点击检查(覆盖查看大图/收起等交互)
      document.addEventListener('click', (e) => {
        const t = e.target;
        if (t.closest('.h-threads-img-tool-btn') || t.closest('.h-threads-img-a')) {
          setTimeout(checkActiveImageAndToggleObserver, 0);
        }
      });
      // Phase3-4:串内/板块自动观察器绑定(mode 策略表)
      // thread:保留末页缓冲 + 大图冻结;board:仅 done 门,不加串内专属能力
      function bindSeamlessAutoLoad(mode) {
        const placeSentinel = (mode === 'board') ? ensureSentinelPlacedBoard : ensureSentinelPlaced;
        if (mode === 'board') {
          // 旧:initObserverBoard 内联(Phase3-2/3-4 收敛)
          // let observer = new IntersectionObserver(...); observer.observe(sentinel);
          observer = bindSeamlessIntersectionObserver(placeSentinel, (entry) => {
            // 原:entry.isIntersecting && !loading && !done + 向下滚动意图
            if (shouldTriggerSeamlessAutoLoad(entry, !done)) {
              loadNextBoard();
            }
          });
          return;
        }
        // thread
        // 旧:内联 IntersectionObserver + 末页/大图冻结(已抽到 Phase3-2 共用绑定)
        // 使用外层 observer 变量(替换原来的 “let observer = ...”)
        observer = bindSeamlessIntersectionObserver(placeSentinel, (entry) => {
          // 新增:若冻结,直接忽略(被大图激活时冻结)
          if (observerFrozen) return;
          // 新增:末页 + 虚拟缓冲区 + 大图激活逻辑
          const maxDomLast = getDomLastPageNum();
          const atLastPage = !!(maxDomLast && lastLoadedPage >= maxDomLast);
          const nearBottom = isSeamlessNearBottom();
          // 检测是否有激活大图且在视口中(限定在当前串的最后 replies)
          const activeInView = isSeamlessElementInViewport(getSeamlessLastRepliesActiveImageBox());
          // 在末页时:如果激活大图且未接近底部 → 冻结观察器避免误触发
          if (atLastPage && activeInView && !nearBottom) {
            try { observer.disconnect(); } catch (e) {}
            observerFrozen = true;
            return;
          }
          // 在末页时:开启“虚拟缓冲区”,只有 nearBottom 才允许触发(解决大图未激活也提前触发的问题)
          if (atLastPage && !nearBottom) {
            return;
          }
          // 原有的触发判定保留
          if (shouldTriggerSeamlessAutoLoad(entry)) {
            loadNext();
          }
        });
      }
      // 兼容旧调用名
      function initObserver() { bindSeamlessAutoLoad('thread'); }
      function initObserverBoard() { bindSeamlessAutoLoad('board'); }
      // Phase3-1:串内/板块手动按钮样式去重
      function createSeamlessLoadMoreButton(onClick) {
        const btn = document.createElement('div');
        btn.className = 'xdex-placeholder';
        btn.textContent = '加载下一页';
        btn.style.cssText = `
          padding: 6px 10px;
          background: rgb(250, 250, 250);
          color: rgb(136, 136, 136);
          border: 1px dashed rgb(187, 187, 187);
          margin: 10px auto;
          cursor: pointer;
          width: 100%;
          box-sizing: border-box;
          text-align: center;
        `;
        btn.addEventListener('click', onClick);
        return btn;
      }
      function placeSeamlessLoadMoreButton(btn, placeSentinel) {
        placeSentinel();
        if (sentinel && sentinel.parentNode) {
          sentinel.parentNode.insertBefore(btn, sentinel);
        }
      }
      // Phase3-4:串内/板块手动按钮绑定对称
      function bindSeamlessManualLoad(mode) {
        // 旧:initManualButton / initManualButtonBoard 两套平行实现
        const placeSentinel = (mode === 'board') ? ensureSentinelPlacedBoard : ensureSentinelPlaced;
        const onClick = (mode === 'board')
          ? (() => loadNextBoard())
          : (() => { loadNext(); });
        // 允许点击时触发 loadNext(Board),由加载函数负责末页提示(避免寂默返回)
        const btn = createSeamlessLoadMoreButton(onClick);
        placeSeamlessLoadMoreButton(btn, placeSentinel);
      }
      // 兼容旧调用名
      function initManualButton() {
        // 旧:内联创建按钮样式(已抽到 createSeamlessLoadMoreButton)
        bindSeamlessManualLoad('thread');
      }
      function initManualButtonBoard() {
        // 旧:与串内按钮同款内联样式(已抽到 createSeamlessLoadMoreButton)
        bindSeamlessManualLoad('board');
      }
      // Phase3-4:启动分支按 mode 策略表,避免 thread/board 四套平行 if
      const seamlessMode = isThreadPage ? 'thread' : (isBoardPage ? 'board' : null);
      if (seamlessMode) {
        if (cfg.enableAutoSeamlessPaging) {
          ensureSeamlessSentinel(seamlessMode);
          bindSeamlessAutoLoad(seamlessMode);
        } else {
          bindSeamlessManualLoad(seamlessMode);
        }
      }
      // 调试:检查 loadNext 是否存在(默认静默;开 xdexSeamlessDebug 才输出)
      seamlessDebugLog('=== 定义 window.SeamlessPaging 前的检查 ===');
      seamlessDebugLog('loadNext 类型:', typeof loadNext);
      seamlessDebugLog('loadNext 函数:', loadNext);
      const loadNextFunc = loadNext;  // ← 先保存引用
      seamlessDebugLog('loadNextFunc 类型:', typeof loadNextFunc);
      // 为拦截中间页发送成功分支提供无缝翻页调用
      window.SeamlessPaging = {
        loadNext: function() {
          seamlessDebugLog('=== window.SeamlessPaging.loadNext 被调用 ===');
          seamlessDebugLog('调用时 loadNext 类型:', typeof loadNext);
          seamlessDebugLog('调用时 loadNextFunc 类型:', typeof loadNextFunc);
          seamlessDebugLog('lastLoadedPage 当前值:', lastLoadedPage);
          seamlessDebugLog('loading 当前值:', loading);
          seamlessDebugLog('loadedPages 内容:', Array.from(loadedPages));
          // 网络加载中或 50ms 重载定时器已排期:忽略重复触发,避免慢网下同一页被重复加载
          if (loading || forceLoadPending) {
            seamlessDebugLog('重复触发翻页:loading 或 forceLoadPending 为 true,忽略');
            return;
          }
          // 旧:内联状态回退(已统一到 prepareForceLoadNext;不传 nextPage 时清 lastLoadedPage+1)
          // loadedPages.delete(lastLoadedPage + 1);   // 清除下一页的已加载标记
          // loading = false;                          // 重置加载状态
          // lastCheckAt = 0;                          // 重置防抖时间戳
          const _forceNext = lastLoadedPage + 1;
          prepareForceLoadNext(); // 与原先 delete(lastLoadedPage+1) 语义一致
          seamlessDebugLog('已删除页码:', _forceNext);
          seamlessDebugLog('loading 重置为:', loading);
          seamlessDebugLog('lastCheckAt 重置为:', lastCheckAt);
          seamlessDebugLog('准备在 50ms 后调用 loadNextFunc');
          forceLoadPending = true;
          setTimeout(() => {
            forceLoadPending = false;
            seamlessDebugLog('=== setTimeout 内部执行 ===');
            seamlessDebugLog('执行前 loadNextFunc 类型:', typeof loadNextFunc);
            try {
              loadNextFunc();
              seamlessDebugLog('loadNextFunc 调用成功');
            } catch (err) {
              console.error('loadNextFunc 调用失败:', err);
            }
          }, 50);
        }
      };
      seamlessDebugLog('=== window.SeamlessPaging 定义完成 ===');
      seamlessDebugLog('window.SeamlessPaging:', window.SeamlessPaging);
      addRefreshButtonIfNeeded();
    } catch (err) {
        console.error('initSeamlessPaging failed', err);
  }
  }
  /* --------------------------------------------------
   * tag 8. 移植‘X岛-揭示板的增强型体验’功能:启用高清图片链接+图片控件+布局调整/串在新标签页打开
   * -------------------------------------------------- */
  /* --------------------------------------------------
  * 合并后的图片处理函数:高清图片 + 防溢出 + 图片控件
  * -------------------------------------------------- */
  // 屏蔽原站点的 initImageBox
  window.initImageBox = function() {
    console.debug("initImageBox 已被屏蔽,由 enableHDImageAndLayoutFix 接管");
  };
  // 新逻辑,启用高清图片链接和布局修正
  const hdImageLazyLoader = (() => {
    // 预热距离固定为最大值:rootMargin 只能在重建 IntersectionObserver 时更改,过去按滚动速度动态调窗
    // 会在滚动中高频 disconnect() + 全文档重新 observe(掉帧主因之一)。远近优先改由 measureImage 的
    // 距离排序承担,不再靠改观察窗实现。
    const ROOT_MARGIN_Y = 2400;
    // 速度采样最小窗(ms):nowTs() 为亚毫秒分辨率,同毫秒内 dt 若被钳成 1ms 会把瞬时速度放大上千倍,
    // 使匀速滚动也被判成急加速。固定采样窗兼作速度上限。
    const SCROLL_SPEED_SAMPLE_MS = 100;
    // 【已停用·注释保留】动态预热窗常量(改固定窗以消除观察器重建风暴)
    // const MIN_ROOT_MARGIN_Y = 600;
    // const BASE_ROOT_MARGIN_Y = 600;
    // const MAX_ROOT_MARGIN_Y = 2400;
    // const LOOKAHEAD_SEC = 1.25;
    // const MARGIN_HYSTERESIS_PX = 100;
    // 非 GIF 原图并发上限;至少预留 1 槽给滚动方向前方,避免视口内大图占满导致下方不预热
    const MAX_CONCURRENT = 3;
    const RESERVE_AHEAD_SLOTS = 1;
    const BEHIND_PENALTY_DISTANCE = 2500;
    let observer = null;
    let scrollListenerBound = false;
    let lastScrollY = window.scrollY || window.pageYOffset || 0;
    let lastScrollTs = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
    let scrollDirection = 1;
    let scrollSpeed = 0; // px/s,EMA
    let currentRootMarginY = ROOT_MARGIN_Y; // 固定窗;保留字段供 getStats 观察(恒定即证明无重建)
    let activeLoads = 0;
    let activeVisibleLoads = 0;
    let activeAheadLoads = 0;
    let activeBehindLoads = 0;
    let peakActiveLoads = 0;
    const queue = [];
    const queued = new Set();
    const loadingKindMap = new WeakMap();
    function getHdUrl(url) {
      const raw = String(url || '');
      return raw.includes('/thumb/') ? raw.replace('/thumb/', '/image/') : raw;
    }
    function isGifUrl(url) {
      return /\.gif(?:$|[?#])/i.test(String(url || ''));
    }
    function nowTs() {
      return (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
    }
    // 【已停用·注释保留】动态窗计算(依赖已停用的速度查表,见 getDesiredRootMarginY)
    // function getDesiredRootMarginY() {
    //   const fromSpeed = Math.round(scrollSpeed * LOOKAHEAD_SEC);
    //   return Math.min(MAX_ROOT_MARGIN_Y, Math.max(MIN_ROOT_MARGIN_Y, BASE_ROOT_MARGIN_Y + fromSpeed));
    // }
    function updateScrollMetrics(currentY) {
      const ts = nowTs();
      const dy = currentY - lastScrollY;
      if (dy > 0) scrollDirection = 1;
      else if (dy < 0) scrollDirection = -1;
      // 方向每次都更新;速度只在跨过最小采样窗时更新,位移自上次提交点累计。
      // 原实现每事件都算并把 dt 钳到 1ms,同毫秒内的多个事件会算出上千倍虚高速度。
      const dt = ts - lastScrollTs;
      if (dt < SCROLL_SPEED_SAMPLE_MS) return;
      const instSpeed = Math.abs(dy) / dt * 1000;
      scrollSpeed = scrollSpeed * 0.7 + instSpeed * 0.3;
      lastScrollY = currentY;
      lastScrollTs = ts;
    }
    // 一次 rect 读取同时得出分类与优先级(原 classifyImage + getImagePriority 的合并,逻辑逐条照搬)
    function measureImage(img, vh) {
      const rect = img.getBoundingClientRect();
      const aheadDistance = scrollDirection >= 0 ? rect.top - vh : -rect.bottom;
      const behindDistance = scrollDirection >= 0 ? -rect.bottom : rect.top - vh;
      const isVisible = rect.bottom >= 0 && rect.top <= vh;
      const kind = isVisible ? 'visible' : (aheadDistance >= 0 ? 'ahead' : (behindDistance > 0 ? 'behind' : 'visible'));
      const imgCenter = rect.top + rect.height / 2;
      const viewportCenter = vh / 2;
      let priority;
      if (isVisible) priority = Math.abs(imgCenter - viewportCenter);
      else if (aheadDistance >= 0) priority = 10000 + aheadDistance;
      else if (behindDistance > 0) priority = (behindDistance > BEHIND_PENALTY_DISTANCE ? 1000000 : 100000) + behindDistance;
      else priority = 200000 + Math.abs(imgCenter - viewportCenter);
      return { kind, priority };
    }
    // 单次遍历产出分类、优先级与 visible/ahead 计数,供本轮 processQueue 全程复用;
    // 取代原“排序逐项回查 rect + 每个候选扫全队列”的 O(N²) 布局读取
    function measureQueue() {
      const __perfToken = startupPerfDebug.begin();
      const meta = new Map();
      const vh = window.innerHeight || document.documentElement.clientHeight || 0;
      let aheadCount = 0;
      let visibleCount = 0;
      for (let i = 0; i < queue.length; i++) {
        const img = queue[i];
        if (!img || !img.isConnected) continue;
        const m = measureImage(img, vh);
        meta.set(img, m);
        if (m.kind === 'ahead') aheadCount++;
        else if (m.kind === 'visible') visibleCount++;
      }
      // ★ 队列长度即 rect 读取次数,是判断成本是否随图片数放大的唯一依据
      startupPerfDebug.finish('hdLazy.measureQueue', __perfToken, () => ({ rects: queue.length, ahead: aheadCount, visible: visibleCount }));
      return { meta, aheadCount, visibleCount };
    }
    function getSnapPriority(snap, img) {
      const m = snap.meta.get(img);
      return m ? m.priority : 0;
    }
    function getSnapKind(snap, img) {
      const m = snap.meta.get(img);
      return m ? m.kind : 'visible';
    }
    function canStartNonGif(kind, snap) {
      const free = MAX_CONCURRENT - activeLoads;
      if (free <= 0) return false;
      if (kind === 'ahead') return true;
      if (kind === 'visible') {
        // 前方仍有待加载时,不为视口内请求吃光最后 RESERVE 个槽
        if (!snap.aheadCount) return true;
        return (free - 1) >= RESERVE_AHEAD_SLOTS || activeAheadLoads > 0;
      }
      // behind:仅当没有 visible/ahead 候选时才填空
      return !snap.visibleCount && !snap.aheadCount;
    }
    function noteLoadStart(kind) {
      activeLoads++;
      peakActiveLoads = Math.max(peakActiveLoads, activeLoads);
      if (kind === 'visible') activeVisibleLoads++;
      else if (kind === 'ahead') activeAheadLoads++;
      else activeBehindLoads++;
    }
    function noteLoadEnd(kind) {
      activeLoads = Math.max(0, activeLoads - 1);
      if (kind === 'visible') activeVisibleLoads = Math.max(0, activeVisibleLoads - 1);
      else if (kind === 'ahead') activeAheadLoads = Math.max(0, activeAheadLoads - 1);
      else activeBehindLoads = Math.max(0, activeBehindLoads - 1);
    }
    // 【已停用·注释保留】观察器重建链:desired 窗改固定 ROOT_MARGIN_Y 后不再需要。
    // 原实现在滚动中高频 disconnect() + 全文档 querySelectorAll + 逐个 observe + 新建 IO。
    // function reobservePendingImages() {
    //   const io = ensureObserver();
    //   if (!io) return;
    //   document.querySelectorAll('img[data-xdex-hd-src]').forEach((img) => {
    //     if (img.dataset.xdexHdLoaded === '1' || img.dataset.xdexHdLoading === '1') return;
    //     io.observe(img);
    //   });
    // }
    // function syncObserverMargin(force) {
    //   const desired = getDesiredRootMarginY();
    //   if (!force && Math.abs(desired - currentRootMarginY) < MARGIN_HYSTERESIS_PX) return;
    //   if (!force && desired === currentRootMarginY) return;
    //   currentRootMarginY = desired;
    //   if (observer) {
    //     try { observer.disconnect(); } catch (e) {}
    //     observer = null;
    //   }
    //   reobservePendingImages();
    // }
    let scrollFrameId = 0;
    function onScrollFrame() {
      const __perfToken = startupPerfDebug.begin();
      const currentY = window.scrollY || window.pageYOffset || 0;
      updateScrollMetrics(currentY);
      processQueue();
      startupPerfDebug.finish('hdLazy.onScrollFrame', __perfToken, () => ({ queue: queue.length, activeLoads }));
    }
    function bindScrollListener() {
      if (scrollListenerBound) return;
      window.addEventListener('scroll', () => {
        // 一帧只处理一次:滚动事件每秒可达数十个,逐事件处理的结果会被后一次覆盖,纯浪费
        if (scrollFrameId) return;
        if (typeof requestAnimationFrame !== 'function') { onScrollFrame(); return; }
        scrollFrameId = requestAnimationFrame(() => { scrollFrameId = 0; onScrollFrame(); });
      }, { passive: true });
      scrollListenerBound = true;
    }
    function ensureObserver() {
      if (observer) return observer;
      if (typeof IntersectionObserver !== 'function') return null;
      observer = new IntersectionObserver(entries => {
        // 整批入队后只处理一次:逐张 enqueue 会让每张图都触发一整套测量+排序
        let added = false;
        entries.forEach(entry => {
          if (!entry.isIntersecting) return;
          const img = entry.target;
          if (enqueue(img, true)) added = true;
          try { observer.unobserve(img); } catch (e) {}
        });
        if (added) processQueue();
      }, {
        root: null,
        rootMargin: `${ROOT_MARGIN_Y}px 0px`,
        threshold: 0.01
      });
      return observer;
    }
    function prepare(img, hdUrl) {
      if (!img) return '';
      const finalHdUrl = getHdUrl(hdUrl || img.currentSrc || img.src || img.getAttribute('src') || '');
      if (!finalHdUrl || !finalHdUrl.includes('/image/')) return '';
      if (!img.dataset.xdexThumbSrc) {
        img.dataset.xdexThumbSrc = img.currentSrc || img.src || img.getAttribute('src') || '';
      }
      img.dataset.xdexHdSrc = finalHdUrl;
      img.loading = 'lazy';
      return finalHdUrl;
    }
    function collect(root) {
      if (!root) return [];
      const imgs = [];
      if (root.matches && root.matches('img[data-xdex-hd-src]')) imgs.push(root);
      if (root.querySelectorAll) {
        root.querySelectorAll('img[data-xdex-hd-src]').forEach(img => imgs.push(img));
      }
      return imgs;
    }
    // 【已停用·注释保留】优先级计算已并入 measureImage(同一次 rect 读取同时给出分类与优先级)
    // function getImagePriority(img) {
    //   const rect = img.getBoundingClientRect();
    //   const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
    //   const viewportCenter = viewportHeight / 2;
    //   const imgCenter = rect.top + rect.height / 2;
    //   const aheadDistance = scrollDirection >= 0 ? rect.top - viewportHeight : -rect.bottom;
    //   const behindDistance = scrollDirection >= 0 ? -rect.bottom : rect.top - viewportHeight;
    //   const isVisible = rect.bottom >= 0 && rect.top <= viewportHeight;
    //   const isAhead = aheadDistance >= 0;
    //   const isBehind = behindDistance > 0;
    //   if (isVisible) return Math.abs(imgCenter - viewportCenter);
    //   if (isAhead) return 10000 + aheadDistance;
    //   if (isBehind) return (behindDistance > BEHIND_PENALTY_DISTANCE ? 1000000 : 100000) + behindDistance;
    //   return 200000 + Math.abs(imgCenter - viewportCenter);
    // }
    // 图片完成回调会在同一批任务里多次触发,每次都跑一轮 measureQueue(N 次 rect 读取)。
    // 合并到下一个宏任务只跑一轮;用 setTimeout 而非 rAF:后台标签页 rAF 不触发会卡住整个队列
    let queueScheduleId = 0;
    function scheduleProcessQueue() {
      if (queueScheduleId) return;
      queueScheduleId = setTimeout(() => { queueScheduleId = 0; processQueue(); }, 0);
    }
    function processQueue() {
      if (queue.length === 0) return;
      // GIF:不占非 GIF 并发槽,直接开载
      for (let i = queue.length - 1; i >= 0; i--) {
        const img = queue[i];
        const hdSrc = img && img.dataset ? img.dataset.xdexHdSrc : '';
        if (hdSrc && isGifUrl(hdSrc)) {
          queue.splice(i, 1);
          queued.delete(img);
          if (img && img.isConnected && img.dataset.xdexHdLoaded !== '1' && img.dataset.xdexHdLoading !== '1') {
            load(img, hdSrc);
          }
        }
      }
      if (queue.length === 0) return;
      // 单次测量:分类、优先级与 ahead/visible 计数一次算完,本轮全程复用
      const snap = measureQueue();
      queue.sort((a, b) => getSnapPriority(snap, a) - getSnapPriority(snap, b));
      // 多轮挑选:跳过暂时因 reserve 不能启动的项,继续尝试后方合适项
      let guard = queue.length + 2;
      while (activeLoads < MAX_CONCURRENT && queue.length > 0 && guard-- > 0) {
        let started = false;
        for (let i = 0; i < queue.length; i++) {
          const img = queue[i];
          if (!img || !img.isConnected) {
            queue.splice(i, 1);
            queued.delete(img);
            started = true;
            break;
          }
          if (img.dataset.xdexHdLoaded === '1' || img.dataset.xdexHdLoading === '1') {
            queue.splice(i, 1);
            queued.delete(img);
            started = true;
            break;
          }
          const hdSrc = img.dataset.xdexHdSrc;
          if (!hdSrc) {
            queue.splice(i, 1);
            queued.delete(img);
            started = true;
            break;
          }
          if (isGifUrl(hdSrc)) {
            queue.splice(i, 1);
            queued.delete(img);
            load(img, hdSrc);
            started = true;
            break;
          }
          const kind = getSnapKind(snap, img);
          if (!canStartNonGif(kind, snap)) continue;
          queue.splice(i, 1);
          queued.delete(img);
          noteLoadStart(kind);
          loadingKindMap.set(img, kind);
          let finished = false;
          const finish = () => {
            if (finished) return;
            finished = true;
            const k = loadingKindMap.get(img) || kind;
            loadingKindMap.delete(img);
            noteLoadEnd(k);
            scheduleProcessQueue();
          };
          img.addEventListener('load', finish, { once: true });
          img.addEventListener('error', finish, { once: true });
          load(img, hdSrc);
          if (img.dataset.xdexHdLoaded === '1' || img.dataset.xdexHdLoading !== '1') finish();
          started = true;
          break;
        }
        if (!started) break;
      }
    }
    function enqueue(img, deferProcess) {
      if (!img || img.dataset.xdexHdLoaded === '1' || img.dataset.xdexHdLoading === '1') return false;
      if (!img.dataset.xdexHdSrc) return false;
      // GIF 直接加载,不进限速队列
      if (isGifUrl(img.dataset.xdexHdSrc)) {
        load(img, img.dataset.xdexHdSrc);
        return false;
      }
      const added = !queued.has(img);
      if (added) {
        queued.add(img);
        queue.push(img);
      }
      // deferProcess=true 时由调用方在整批入队后统一处理一次
      if (!deferProcess) processQueue();
      return added;
    }
    function observe(root) {
      bindScrollListener();
      const io = ensureObserver();
      if (!io) return;
      collect(root).forEach(img => {
        if (img.dataset.xdexHdLoaded === '1') return;
        // GIF:观察前即可直接开载
        if (img.dataset.xdexHdSrc && isGifUrl(img.dataset.xdexHdSrc)) {
          load(img, img.dataset.xdexHdSrc);
          return;
        }
        io.observe(img);
      });
    }
    function load(img, hdUrl) {
      if (!img) return;
      const finalHdUrl = prepare(img, hdUrl);
      if (!finalHdUrl) return;
      if ((img.currentSrc || img.src) === finalHdUrl) {
        img.dataset.xdexHdLoaded = '1';
        delete img.dataset.xdexHdLoading;
        return;
      }
      if (img.dataset.xdexHdLoading === '1') return;
      img.dataset.xdexHdLoading = '1';
      const thumbSrc = img.dataset.xdexThumbSrc || '';
      const onLoad = () => {
        img.dataset.xdexHdLoaded = '1';
        delete img.dataset.xdexHdLoading;
      };
      const onError = () => {
        delete img.dataset.xdexHdLoading;
        if (thumbSrc && (img.currentSrc || img.src) === finalHdUrl) img.src = thumbSrc;
      };
      img.addEventListener('load', onLoad, { once: true });
      img.addEventListener('error', onError, { once: true });
      img.src = finalHdUrl;
    }
    function getStats() {
      return {
        activeLoads,
        activeVisibleLoads,
        activeAheadLoads,
        activeBehindLoads,
        peakActiveLoads,
        queued: queue.length,
        maxConcurrent: MAX_CONCURRENT,
        reserveAheadSlots: RESERVE_AHEAD_SLOTS,
        scrollDirection,
        scrollSpeed: Math.round(scrollSpeed),
        rootMarginY: currentRootMarginY
      };
    }
    return { getHdUrl, isGifUrl, prepare, observe, load, getStats };
  })();
  const hdImageDebugTarget = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

  function clearHDImageTiming() {
    if (performance && typeof performance.clearResourceTimings === 'function') {
      performance.clearResourceTimings();
    }
    return 'cleared';
  }
  function reportHDImageConcurrency(options = {}) {
    const includeGif = !!options.includeGif;
    const showTable = options.table !== false;
    const entries = performance.getEntriesByType('resource')
      .filter(e => e && e.initiatorType === 'img')
      .filter(e => String(e.name || '').includes('/image/'))
      .filter(e => includeGif || !/\.gif(?:$|[?#])/i.test(String(e.name || '')))
      .filter(e => Number.isFinite(e.startTime) && Number.isFinite(e.responseEnd) && e.responseEnd > e.startTime)
      .sort((a, b) => a.startTime - b.startTime);
    let peak = 0;
    let peakAt = 0;
    entries.forEach(a => {
      let active = 0;
      entries.forEach(b => {
        if (b.startTime <= a.startTime && b.responseEnd >= a.startTime) active++;
      });
      if (active > peak) {
        peak = active;
        peakAt = a.startTime;
      }
    });
    const rows = entries.map(e => ({
      start: Math.round(e.startTime),
      end: Math.round(e.responseEnd),
      duration: Math.round(e.duration),
      url: String(e.name || '').slice(-100)
    }));
    if (showTable && rows.length) console.table(rows);
    const result = {
      peakResourceOverlap: peak,
      peakAt: Math.round(peakAt),
      countedRequests: entries.length,
      includeGif,
      loader: hdImageLazyLoader.getStats()
    };
    console.log('[XDEX HD image concurrency]', result);
    return result;
  }
  window.__xdexClearHDImageTiming = clearHDImageTiming;
  window.__xdexReportHDImageConcurrency = reportHDImageConcurrency;
  hdImageDebugTarget.__xdexClearHDImageTiming = clearHDImageTiming;
  hdImageDebugTarget.__xdexReportHDImageConcurrency = reportHDImageConcurrency;

  function isSettingsPanelImageEnhancementRoot(root) {
    if (!root || root === document) return false;
    const node = root.nodeType === 1 ? root : root.parentElement;
    return !!(node && node.closest && node.closest('#sp_panel'));
  }
  function enableHDImageAndLayoutFix(root = document) {
    return startupPerfDebug.measure('enableHDImageAndLayoutFix', () => {
    if (isSettingsPanelImageEnhancementRoot(root)) return;
    const isDocumentRoot = root === document;
    if (isDocumentRoot && enableHDImageAndLayoutFix.__documentProcessed) {
      handlePendingHDImageAndLayoutFix(document);
      return;
    }
    if (isDocumentRoot) enableHDImageAndLayoutFix.__documentProcessed = true;
    // ==================== 注入样式(只注入一次)====================
    if (!document.getElementById('prevent-overflow-style')) {
      const style = document.createElement('style');
      style.id = 'prevent-overflow-style';
      style.textContent = `
        .h-threads-content,
        .h-threads-item-reply-main,
        .h-preview-box {
          max-width: 100%;
          overflow-wrap: break-word;
          word-wrap: break-word;
          word-break: break-word;
          box-sizing: border-box;
        }
        .h-threads-img-box.h-active {
          max-width: 100%;
          box-sizing: border-box;
          overflow: visible;
        }
        .h-threads-img-box.h-active .h-threads-img-a {
          display: block;
          position: relative;
          overflow: hidden;
          box-sizing: border-box;
          transition: width 0.3s ease, height 0.3s ease;
        }
        .h-threads-img-box.h-active .h-threads-img {
          display: block;
          box-sizing: border-box;
          position: relative;
          transform-origin: center center;
          transition: transform 0.2s ease-out,
                      margin 0.2s ease-out;
        }
        .h-threads-content img:not(.h-threads-img),
        .h-preview-box img {
          max-width: 100% !important;
          height: auto !important;
          box-sizing: border-box;
        }
        .h-threads-content pre,
        .h-preview-box pre {
          max-width: 100%;
          overflow-x: auto;
          white-space: pre-wrap;
          word-wrap: break-word;
          box-sizing: border-box;
        }
        .h-threads-content table,
        .h-preview-box table {
          max-width: 100%;
          overflow-x: auto;
          display: block;
          box-sizing: border-box;
        }
        .h-threads-content a,
        .h-preview-box a {
          word-break: break-all;
          overflow-wrap: break-word;
        }
      `;
      document.head.appendChild(style);
    }
    // ==================== 子函数1: 布局计算和溢出处理 ====================
    const handleImageLayout = {
      getElementContentWidth(el) {
        if (!el) return 0;
        const style = getComputedStyle(el);
        const paddingX = (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.paddingRight) || 0);
        const width = el.clientWidth || el.offsetWidth || 0;
        return Math.max(0, Math.floor(width - paddingX));
      },
      // ★ 新增:根据场景(板块页/串内页)计算消息容器最大允许宽度
      getMaxMsgWidth(msgMain) {
        // ===== 新增:引用浮窗场景 =====
        const quoteBox = msgMain.closest('.qp-quote');
        if (quoteBox) {
          return this.getElementContentWidth(quoteBox);
        }
        // 基础浏览器边缘限制
        const viewportLimit = window.innerWidth - 240; // 保留你的全局边距逻辑
        // 串内页默认上限
        const threadPageCap = 1200;
        // 判断是否处于板块页的回复结构(存在 .h-threads-item-index)
        const threadItem = msgMain.closest('.h-threads-item');
        const isBoardPage = threadItem && threadItem.classList.contains('h-threads-item-index');
        if (!isBoardPage) {
          // 串内页:使用原有逻辑(viewport 边缘 + 1200 上限)
          return Math.min(viewportLimit, threadPageCap);
        }
        // 板块页:右侧区域限制
        // 右侧区域宽度 = 整个串容器宽度 - 左侧回复序号图标宽度 - 留边距40
        const iconEl = msgMain.previousElementSibling && msgMain.previousElementSibling.classList.contains('h-threads-item-reply-icon')
          ? msgMain.previousElementSibling
          : null;
        const iconWidth = iconEl ? iconEl.offsetWidth || 0 : 0;
        const threadWidth = threadItem.offsetWidth || viewportLimit;
        const rightRegionWidth = Math.max(0, threadWidth - iconWidth - 40);
        // 不能超过浏览器边缘限制
        return Math.min(rightRegionWidth, viewportLimit);
      },
      // ★ 新增:未激活时也拓展消息容器宽度
      expandMsgWidthIfImageExists(msgMain) {
        const imgBox = msgMain.querySelector('.h-threads-img-box');
        if (!imgBox) return; // 没有图片则跳过
        // 预览框为空壳预览,不参与未激活加宽;否则早期注入时冻结的 px 与正式预览框重算值不一致,替换时会看到宽度跳变
        if (msgMain.closest('.h-preview-box')) return;
        // ☆ 新增:检查是否已经扩展过,如果已扩展则跳过
        if (msgMain.__imageWidthExpanded === true) return;
        // 如果图片未激活
        if (!imgBox.classList.contains('h-active')) {
          const currentWidth = msgMain.offsetWidth;
          // ★ 未参与布局(隐藏/游离)时量不到宽度,此刻不做判定也不置锁
          if (!currentWidth) {
            // ★ 事件驱动补判定:pending 队列对每个节点只处理一次,若插入时容器尚不可测量(所在容器
            // 隐藏/未布局完成),该回复要等下一次全文档 pass(window.load 或触底增量更新)才有机会
            // 补写,期间一直保持窄列。这里用 ResizeObserver 精确等「容器从 0 宽变为有实际尺寸」的
            // 那一次回调:量到宽度即按正常流程写入并置锁、随后断开观察;仍为 0 宽(继续隐藏)则不
            // 动作也不轮询。永久隐藏的回复只会有一次 0 尺寸初始回调,无后续开销。
            if (typeof ResizeObserver === 'function' && !msgMain.__xdexExpandWidthRO) {
              // ★ 计数埋点:本分支为「容器未布局」的兜底,每个未布局容器挂一个;
              // 数量即「未布局带图回复数」,多图长串应关注其总数
              startupPerfDebug.record('observer.create:expandWidthRO', 0, { tag: 'msg-main' });
              const ro = new ResizeObserver(() => {
                if (!msgMain.offsetWidth) return; // 仍不可测量(含 observe 时的 0 尺寸初始回调)
                ro.disconnect();
                msgMain.__xdexExpandWidthRO = null;
                if (msgMain.isConnected && msgMain.__imageWidthExpanded !== true) {
                  handleImageLayout.expandMsgWidthIfImageExists(msgMain);
                }
              });
              ro.observe(msgMain);
              msgMain.__xdexExpandWidthRO = ro;
            }
            return;
          }
          const maxMsgWidth = this.getMaxMsgWidth(msgMain);
          // ★ 加宽量 = 固定预算(getExpandedWidthBudget):按「本步骤之后仍会改变容器宽度的操作」的
          // 最坏叠加一次性算入并写入,不做实时测量,之后不再重算,避免渲染后的反复横向变化
          const targetWidth = Math.min(currentWidth + this.getExpandedWidthBudget(), maxMsgWidth);
          // ★ 目标宽度非法则不写入也不置锁(写入无效值时浏览器会丢弃该声明,锁却已置位 → 该回复永久不加宽)
          if (!Number.isFinite(targetWidth) || targetWidth <= 0) return;
          // 保存原始宽度,便于之后恢复
          if (msgMain.__originalWidth === undefined) {
            msgMain.__originalWidth = msgMain.style.width || '';
          }
          msgMain.style.width = targetWidth + 'px';
          // ☆ 新增:标记已经扩展过,防止重复调用时继续加宽(仅在成功写入宽度后置位)
          msgMain.__imageWidthExpanded = true;
        }
      },
      // ★ 加宽预算(固定常数,不做实时测量):按「本步骤之后仍会改变 main 容器宽度的操作」的最坏叠加取值,
      // 一次写入后不再重算,避免渲染后的反复横向变化。构成:
      // - 图片加载形态切换 ≈ 200px:原图(image/)与缩略图(thumb/)两种形式的非激活预览宽度均为固定值
      //   (原图 ≈ 270px,缩略图按长边缩放、竖长图最窄 ≈ 70px,如 PDF 长页 90 → 270),最坏差 ≈ 200px
      // - 相对时间形态演化 ≈ 101px:最短形态「今天(四)N秒前」(≈5.7em) → 最坏「N年前 YYYY-MM-DD(四)HH:MM:SS」
      //   (≈13.4em),差 ≈ 7.75em,按信息行 ~13px 字号折算(跨年只变数字,宽度不再增长)
      // - 饼干大小写 ≈ 18px:饼干为 7 位,全大写 ID 在非等宽字体下比小写宽(按 7 字符 ID 估)
      // - 饼干标记内边距 6px:markAllCookies 给 ID 文本加 0 3px,可能晚于本步骤生效
      // 不产生增量的项(预算 0):编号/Po 标记(只改 3em 定宽的图标单元格)、去除无标题/无名氏(只收窄,
      // 且已在本步骤前生效)。观感调整只需改这一个返回值。
      getExpandedWidthBudget() {
        return (270 - 70) + 101 + 18 + 6; // ≈ 325px(270/70 = 原图/缩略图形式的预览宽常数)
      },
      // 预计算图片在所有旋转角度下的尺寸
      precalculateImageSizes(naturalWidth, naturalHeight, maxWidth) {
        const sizes = {};
        // 0° 和 180°(宽高不变)
        let horizontal = {
          containerWidth: naturalWidth,
          containerHeight: naturalHeight,
          imgWidth: naturalWidth,
          imgHeight: naturalHeight,
          scale: 1
        };
        // 判断当前朝向下,应该按哪个维度适配
        if (naturalWidth > maxWidth) {
          // 宽度超限,按宽度等比缩放
          const scale = maxWidth / naturalWidth;
          horizontal.containerWidth = maxWidth;
          horizontal.containerHeight = Math.floor(naturalHeight * scale);
          horizontal.imgWidth = maxWidth;
          horizontal.imgHeight = Math.floor(naturalHeight * scale);
          horizontal.scale = scale;
        // } else if (naturalHeight > maxWidth && naturalWidth < maxWidth * 0.8) {
        //   // 宽度明显小于最大宽度(小于80%)且高度超限时,才按高度等比缩放
        //   const scale = maxWidth / naturalHeight;
        //   horizontal.containerWidth = Math.floor(naturalWidth * scale);
        //   horizontal.containerHeight = maxWidth;
        //   horizontal.imgWidth = Math.floor(naturalWidth * scale);
        //   horizontal.imgHeight = maxWidth;
        //   horizontal.scale = scale;
        // }
          } else {
            // 宽度未超限,保持原尺寸或适当放大
            // 不做任何处理,使用初始值(原尺寸)
          }
        // 如果宽高都不超限,保持原尺寸(已在初始化时设置)
        sizes[0] = sizes[180] = horizontal;
        // 90° 和 270°(宽高互换)
        let vertical = {
          containerWidth: naturalHeight,   // 容器宽度 = 原图高度
          containerHeight: naturalWidth,   // 容器高度 = 原图宽度
          imgWidth: naturalWidth,
          imgHeight: naturalHeight,
          scale: 1
        };
        // 旋转后,容器的宽度实际是原图的高度
        if (naturalHeight > maxWidth) {
          // 旋转后宽度(原图高度)超限,按原图高度等比缩放
          const scale = maxWidth / naturalHeight;
          vertical.containerWidth = maxWidth;
          vertical.containerHeight = Math.floor(naturalWidth * scale);
          vertical.imgWidth = Math.floor(naturalWidth * scale);
          vertical.imgHeight = maxWidth;
          vertical.scale = scale;
        } else if (naturalWidth > maxWidth) {
          // 旋转后宽度未超限但高度(原图宽度)超限,按原图宽度等比缩放
          const scale = maxWidth / naturalWidth;
          vertical.containerWidth = Math.floor(naturalHeight * scale);
          vertical.containerHeight = maxWidth;
          vertical.imgWidth = maxWidth;
          vertical.imgHeight = Math.floor(naturalHeight * scale);
          vertical.scale = scale;
        }
        // 如果宽高都不超限,保持原尺寸(已在初始化时设置)
        sizes[90] = sizes[270] = vertical;
        return sizes;
      },
      // 应用预计算的尺寸
      applyImageSize(imgBox, rotateIndex) {
        const imgA = imgBox.querySelector('.h-threads-img-a');
        const img = imgBox.querySelector('.h-threads-img');
        if (!imgA || !img || !imgBox.__sizeCache) return;
        img.removeAttribute('align');
        img.removeAttribute('hspace');
        imgBox.style.float = 'none';
        imgA.style.float = 'none';
        img.style.float = 'none';
        // 强制重置图片定位为 relative,确保在容器内正确定位
        img.style.position = 'relative';
        img.style.top = 'auto';
        img.style.left = 'auto';
        // 根据 rotateIndex 确定旋转角度
        // const rotation = (rotateIndex * 90) % 360;
        // 不取模,保持角度单调累加
        const rotation = rotateIndex * 90;
        // 用于查找尺寸缓存的归一化角度
        const normalized = ((rotation % 360) + 360) % 360;
        let targetRotation;
        if (normalized === 0) targetRotation = 0;
        else if (normalized === 90) targetRotation = 90;
        else if (normalized === 180) targetRotation = 180;
        else if (normalized === 270) targetRotation = 270;
        else targetRotation = 0;
        // 归一化到 0, 90, 180, 270
        // let targetRotation;
        // if (rotation === 0) {
        //   targetRotation = 0;
        // } else if (rotation === 90) {
        //   targetRotation = 90;
        // } else if (rotation === 180) {
        //   targetRotation = 180;
        // } else if (rotation === 270) {
        //   targetRotation = 270;
        // } else {
        //   targetRotation = 0;
        // }
        const size = imgBox.__sizeCache[targetRotation];
        if (!size) return;
        const isRotated90or270 = (targetRotation === 90 || targetRotation === 270);
        // 设置容器尺寸(容器不旋转,只改变宽高)
        imgA.style.width = size.containerWidth + 'px';
        imgA.style.height = size.containerHeight + 'px';
        imgA.style.maxWidth = imgBox.__maxWidth + 'px';
        // 设置图片尺寸
        img.style.width = size.imgWidth + 'px';
        img.style.height = size.imgHeight + 'px';
        img.style.maxWidth = 'none';
        img.style.maxHeight = 'none';
        // 图片只需要旋转,不需要translate偏移(因为容器已经是正确尺寸)
        img.style.transform = `rotate(${rotation}deg) scale(1.02)`;
        setTimeout(() => {
          img.style.transform = `rotate(${rotation}deg) scale(1)`;
        }, 50);
        if (isRotated90or270) {
          const marginTop = (size.containerHeight - size.imgHeight) / 2;
          const marginLeft = (size.containerWidth - size.imgWidth) / 2;
          img.style.marginTop = marginTop + 'px';
          img.style.marginLeft = marginLeft + 'px';
        } else {
          img.style.display = '';
          img.style.marginTop = '0px';
          img.style.marginLeft = '0px';
          img.style.marginRight = '0px';
          img.style.marginBottom = '0px';
        }
        // 使用margin居中图片
        // if (isRotated90or270) {
        //   const marginTop = (size.containerHeight - size.imgHeight) / 2;
        //   let marginLeft = (size.containerWidth - size.imgWidth) / 2;
        //   // ★ 修正:避免负 margin 导致图片超出容器
        //   if (marginLeft < 0) marginLeft = 0;
        //   img.style.marginTop = marginTop + 'px';
        //   img.style.marginLeft = marginLeft + 'px';
        // } else {
        //   img.style.marginTop = '0px';
        //   img.style.marginLeft = '0px';
        // }
        // 设置容器和图片尺寸保持一致
        // if (isRotated90or270) {
        //   // 旋转 90°/270° 时,容器宽高直接跟随图片
        //   imgA.style.width = size.imgWidth + 'px';
        //   imgA.style.height = size.imgHeight + 'px';
        // } else {
        //   // 0°/180° 时,容器用正常计算值
        //   imgA.style.width = size.containerWidth + 'px';
        //   imgA.style.height = size.containerHeight + 'px';
        // }
        // imgA.style.maxWidth = imgBox.__maxWidth + 'px';
        // // 设置图片尺寸
        // img.style.width = size.imgWidth + 'px';
        // img.style.height = size.imgHeight + 'px';
        // img.style.maxWidth = 'none';
        // img.style.maxHeight = 'none';
      },
      // 处理激活状态的图片盒子
      handleActiveImageBox(imgBox, forceRecalculate = false) {
        const imgA = imgBox.querySelector('.h-threads-img-a');
        const img = imgBox.querySelector('.h-threads-img');
        if (!imgA || !img) return;
        const isActive = imgBox.classList.contains('h-active');
        if (!isActive) {
          if (imgBox.__activeLayoutFrame) {
            cancelAnimationFrame(imgBox.__activeLayoutFrame);
            imgBox.__activeLayoutFrame = 0;
          }
          // 取消激活:恢复原始状态并清除缓存
          if (imgBox.__originalStyles) {
            imgA.style.width = imgBox.__originalStyles.aWidth || '';
            imgA.style.height = imgBox.__originalStyles.aHeight || '';
            imgA.style.maxWidth = imgBox.__originalStyles.aMaxWidth || '';
            img.style.width = imgBox.__originalStyles.imgWidth || '';
            img.style.height = imgBox.__originalStyles.imgHeight || '';
            img.style.maxWidth = imgBox.__originalStyles.imgMaxWidth || '';
            img.style.maxHeight = imgBox.__originalStyles.imgMaxHeight || '';
            img.style.transform = imgBox.__originalStyles.imgTransform || '';
            img.style.position = imgBox.__originalStyles.imgPosition || '';
            img.style.top = imgBox.__originalStyles.imgTop || '';
            img.style.left = imgBox.__originalStyles.imgLeft || '';
            img.style.display = imgBox.__originalStyles.imgDisplay || '';
            img.style.marginTop = imgBox.__originalStyles.imgMarginTop || '';
            img.style.marginLeft = imgBox.__originalStyles.imgMarginLeft || '';
            img.style.marginRight = imgBox.__originalStyles.imgMarginRight || '';
            img.style.marginBottom = imgBox.__originalStyles.imgMarginBottom || '';
            imgBox.style.float = imgBox.__originalStyles.boxFloat || '';
            imgA.style.float = imgBox.__originalStyles.aFloat || '';
            img.style.float = imgBox.__originalStyles.imgFloat || '';
            if (imgBox.__originalStyles.imgAlign == null) img.removeAttribute('align');
            else img.setAttribute('align', imgBox.__originalStyles.imgAlign);
            if (imgBox.__originalStyles.imgHspace == null) img.removeAttribute('hspace');
            else img.setAttribute('hspace', imgBox.__originalStyles.imgHspace);
            delete imgBox.__originalStyles;
            delete imgBox.__sizeCache;
            delete imgBox.__maxWidth;
          }
          // ★ 未激活但存在图片时,消息容器宽度扩大 100px,且不超过场景最大宽度
          // const msgMain = imgBox.closest('.h-threads-item-reply-main');
          // if (msgMain) {
          //   const currentWidth = msgMain.offsetWidth;
          //   const maxMsgWidth = this.getMaxMsgWidth(msgMain);
          //   const targetWidth = Math.min(currentWidth + 50, maxMsgWidth);
          //   msgMain.style.width = targetWidth + 'px';
          // }
          // ★ 新增逻辑:恢复页面加载时的额外拓展
          // const msgMain = imgBox.closest('.h-threads-item-reply-main');
          // if (msgMain && msgMain.__originalWidth !== undefined) {
          //   msgMain.style.width = msgMain.__originalWidth; // 恢复原始宽度
          //   delete msgMain.__originalWidth;
          // }
          return;
        }
        // 保存原始样式
        if (!imgBox.__originalStyles) {
          imgBox.__originalStyles = {
            aWidth: imgA.style.width,
            aHeight: imgA.style.height,
            aMaxWidth: imgA.style.maxWidth,
            imgWidth: img.style.width,
            imgHeight: img.style.height,
            imgMaxWidth: img.style.maxWidth,
            imgMaxHeight: img.style.maxHeight,
            imgTransform: img.style.transform,
            imgPosition: img.style.position,
            imgTop: img.style.top,
            imgLeft: img.style.left,
            imgDisplay: img.style.display,
            imgMarginTop: img.style.marginTop,
            imgMarginLeft: img.style.marginLeft,
            imgMarginRight: img.style.marginRight,
            imgMarginBottom: img.style.marginBottom,
            boxFloat: imgBox.style.float,
            aFloat: imgA.style.float,
            imgFloat: img.style.float,
            imgAlign: img.getAttribute('align'),
            imgHspace: img.getAttribute('hspace')
          };
        }
        if (imgBox.__activeLayoutFrame) cancelAnimationFrame(imgBox.__activeLayoutFrame);
        imgBox.__activeLayoutForceRecalculate = !!(imgBox.__activeLayoutForceRecalculate || forceRecalculate);
        imgBox.__activeLayoutFrame = requestAnimationFrame(() => {
          const shouldForceRecalculate = !!imgBox.__activeLayoutForceRecalculate;
          imgBox.__activeLayoutFrame = 0;
          imgBox.__activeLayoutForceRecalculate = false;
          if (!imgBox.classList.contains('h-active')) return;
          const naturalWidth = img.naturalWidth;
          const naturalHeight = img.naturalHeight;
          if (!naturalWidth || !naturalHeight) {
            if (img.complete) return;
            img.onload = () => {
              if (imgBox.classList.contains('h-active')) this.handleActiveImageBox(imgBox);
            };
            return;
          }
          // 强制触发布局更新
          document.body.offsetHeight;
          // 获取当前最大可用宽度
          // const container = imgBox.closest('.h-threads-item-reply-main');
          const container = imgBox.closest('.h-threads-item-reply-main') || imgBox.closest('.h-threads-item-main');
          let maxWidth;
          if (container) {
            container.offsetHeight; // 触发 reflow
            let msgWidth = container.offsetWidth || container.clientWidth;
          // 新增:识别预览框与浮窗场景
          const isPreview = !!imgBox.closest('.h-preview-box');
          const isOverlay = !!imgBox.closest('.qp-body'); // 回复浮窗
          if (isPreview) {
            if (isOverlay) {
              // 在预览框 + 浮窗中:使用浮窗内容容器宽度
              const quoteBox = imgBox.closest('.qp-quote');
              if (quoteBox) {
                maxWidth = this.getElementContentWidth(quoteBox);
              } else {
                const wrapEl = imgBox.closest('.qp-content-wrap');
                if (wrapEl) {
                  msgWidth = wrapEl.offsetWidth || msgWidth;
                }
                // 留边距 40(与原逻辑一致),不再受 window.innerWidth - 240 限制
                maxWidth = Math.max(0, (msgWidth || 0) - 40);
              }
            } else {
              // 在预览框但不在浮窗中:最大宽度限制为 800
              // 仍保留消息容器边距 40
              const base = Math.max(0, (msgWidth || 0) - 40);
              maxWidth = Math.min(base, 800);
            }
          } else {
            // 常规消息:保留原逻辑
            if (msgWidth < 300) {
              const parentContainer = container.closest('.h-threads-item-replies') ||
                                      container.closest('.h-threads-list') ||
                                      container.parentElement;
              if (parentContainer) {
                msgWidth = parentContainer.offsetWidth || window.innerWidth - 240;
              } else {
                msgWidth = window.innerWidth - 240;
              }
            }
            // maxWidth = Math.min(msgWidth - 40, handleImageLayout.getMaxMsgWidth(container));
            maxWidth = handleImageLayout.getMaxMsgWidth(container);
          }
        } else {
          // 无容器时,兜底用视口宽度
          maxWidth = Math.min(window.innerWidth - 240, 1200);
        }
          // 如果最大宽度变化或首次计算,重新预计算所有旋转角度的尺寸
          if (!imgBox.__sizeCache || shouldForceRecalculate || imgBox.__maxWidth !== maxWidth) {
            imgBox.__maxWidth = maxWidth;
            imgBox.__sizeCache = this.precalculateImageSizes(naturalWidth, naturalHeight, maxWidth);
          }
          // 获取当前旋转索引
          const rotateIndex = parseInt(img.dataset.rotateIndex || '0');
          // 应用对应旋转角度的尺寸
          this.applyImageSize(imgBox, rotateIndex);
        });
      },
      // 处理普通元素溢出
      handleGeneralElements(container) {
        const SELECTORS = [
          '.h-threads-content',
          '.h-threads-item-reply-main',
          '.h-preview-box',
          'pre',
          'code',
          'table'
        ];
        SELECTORS.forEach(selector => {
          container.querySelectorAll(selector).forEach(el => {
            const rect = el.getBoundingClientRect();
            if (rect.right > window.innerWidth) {
              el.style.maxWidth = '100%';
              el.style.boxSizing = 'border-box';
            }
            if (el.tagName === 'PRE' || el.tagName === 'CODE') {
              el.style.maxWidth = '100%';
              el.style.overflowX = 'auto';
              el.style.whiteSpace = 'pre-wrap';
            }
            if (el.tagName === 'TABLE') {
              el.style.maxWidth = '100%';
              el.style.display = 'block';
              el.style.overflowX = 'auto';
            }
          });
        });
        container.querySelectorAll('.h-threads-content a, .h-preview-box a').forEach(el => {
          const href = el.getAttribute('href') || '';
          if (href.length > 50) {
            el.style.wordBreak = 'break-all';
            el.style.overflowWrap = 'break-word';
          }
        });
        container.querySelectorAll('.h-threads-content img:not(.h-threads-img), .h-preview-box img').forEach(el => {
          el.style.maxWidth = '100%';
          el.style.height = 'auto';
        });
      }
    };
    // ==================== 子函数2: 交互逻辑处理 ====================
    const handleImageInteraction = {
      // 记录点击次数(用于切换展开/收起)
      clickCountMap: new WeakMap(),
      lastClickedAnchor: null,
      // 准备高清地址,实际图片请求交给近视窗懒加载。
      replaceHDLinks(container) {
        const imgs = [];
        if (container.matches && container.matches('img')) imgs.push(container);
        container.querySelectorAll('img').forEach(img => imgs.push(img));
        imgs.forEach(img => {
          if (img.closest('.h-preview-box')) return; // 新增:预览框中跳过
          const src = img.currentSrc || img.src || img.getAttribute('src') || '';
          if (src.includes('/thumb/')) {
            const hdSrc = hdImageLazyLoader.prepare(img, src);
            if (hdImageLazyLoader.isGifUrl(hdSrc)) hdImageLazyLoader.load(img, hdSrc);
          }
        });
        const anchors = [];
        if (container.matches && container.matches('a')) anchors.push(container);
        container.querySelectorAll('a').forEach(a => anchors.push(a));
        anchors.forEach(a => {
          if (a.closest('.h-preview-box')) return; // 新增:预览框中跳过
          if (a.href.includes('/thumb/')) {
            a.href = hdImageLazyLoader.getHdUrl(a.href);
          }
        });
      },
      // 绑定图片点击展开/收起逻辑
      bindImageClickLogic(container) {
        container.querySelectorAll('.h-threads-img-a').forEach(anchor => {
          // ===== 新增:预览框内的图片不绑定点击逻辑 =====
          if (anchor.closest('.h-preview-box')) return;
          if (anchor.dataset.hdBound === "1") return;
          anchor.dataset.hdBound = "1";
          anchor.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopImmediatePropagation();
            e.stopPropagation();
          const box = anchor.closest('.h-threads-img-box');
          if (!box) return;
          const img = box.querySelector('.h-threads-img');
          if (!img) return;
          if (!box.__initialDisplayState) {
            box.__initialDisplayState = {
              src: img.currentSrc || img.src || '',
              href: anchor.href || '',
              aWidth: anchor.style.width || '',
              aHeight: anchor.style.height || '',
              imgWidth: img.style.width || '',
              imgHeight: img.style.height || '',
              imgNaturalWidth: img.width || 0,
              imgNaturalHeight: img.height || 0
            };
          }
          const shouldOpen = !box.classList.contains('h-active');
          // 同步保留旧计数状态,但展开/收起以真实 DOM 状态为准。
          if (this.lastClickedAnchor !== anchor) {
            this.lastClickedAnchor = anchor;
            this.clickCountMap.set(anchor, 0);
            }
            this.clickCountMap.set(anchor, shouldOpen ? 1 : 0);
            if (shouldOpen) {
              // 展开
              box.classList.add('h-active');
              hdImageLazyLoader.load(img, anchor.href);
              img.dataset.rotateIndex = '0'; // 重置旋转索引
              // ★ 新增:加载失败兜底
              img.onerror = () => {
                console.warn('[图片加载失败,回退到缩略图]', img.src);
                const fallback = img.dataset.xdexThumbSrc || img.getAttribute('data-src') || anchor.href;
                img.src = fallback;
              };
              // 触发布局计算
              handleImageLayout.handleActiveImageBox(box);
            } else {
              // 收起
              box.classList.remove('h-active');
              img.style.transform = '';
              img.dataset.rotateIndex = '0';
              const initial = box.__initialDisplayState;
              if (initial) {
                if (initial.src) img.src = initial.src;
                if (initial.href) anchor.href = initial.href;
                anchor.style.width = initial.aWidth || '';
                anchor.style.height = initial.aHeight || '';
                img.style.width = initial.imgWidth || '';
                img.style.height = initial.imgHeight || '';
                if (!initial.imgWidth && initial.imgNaturalWidth) {
                  img.width = initial.imgNaturalWidth;
                }
                if (!initial.imgHeight && initial.imgNaturalHeight) {
                  img.height = initial.imgNaturalHeight;
                }
              }
              // 触发布局恢复
              handleImageLayout.handleActiveImageBox(box);
            }
          }, true);
        });
      },
      // 绑定图片控件(收起/旋转按钮)
      bindImageControls(container) {
        container.querySelectorAll('.h-threads-img-box').forEach(box => {
          // ===== 新增:预览框内的图片不绑定控件逻辑 =====
          if (box.closest('.h-preview-box')) return;
          if (box.dataset.toolBound === "1") return;
          box.dataset.toolBound = "1";
          const img = box.querySelector('.h-threads-img');
          const imgA = box.querySelector('.h-threads-img-a');
          const toolSmall = box.querySelector('.h-threads-img-tool-small');
          const toolLeft = box.querySelector('.h-threads-img-tool-left');
          const toolRight = box.querySelector('.h-threads-img-tool-right');
          // 收起按钮
          if (toolSmall && imgA) {
            toolSmall.addEventListener('click', (e) => {
              e.preventDefault();
              e.stopImmediatePropagation();
              imgA.click();
            });
          }
          // 左旋按钮(逆时针)
          if (toolLeft && img) {
            toolLeft.addEventListener('click', (e) => {
              e.preventDefault();
              e.stopImmediatePropagation();
              if (!box.classList.contains('h-active')) return;
              let rotateIndex = parseInt(img.dataset.rotateIndex || '0');
              //rotateIndex = (rotateIndex - 1 + 4) % 4;
              rotateIndex = rotateIndex - 1;   // ★ 改为累加,不取模
              img.dataset.rotateIndex = rotateIndex.toString();
              console.log('[左旋] rotateIndex:', rotateIndex);
              // 延迟执行布局调整,等待旋转动画完成
              setTimeout(() => {
                handleImageLayout.applyImageSize(box, rotateIndex);
              }, 50);
            });
          }
          // 右旋按钮(顺时针)
          if (toolRight && img) {
            toolRight.addEventListener('click', (e) => {
              e.preventDefault();
              e.stopImmediatePropagation();
              if (!box.classList.contains('h-active')) return;
              let rotateIndex = parseInt(img.dataset.rotateIndex || '0');
              //rotateIndex = (rotateIndex + 1) % 4;
              rotateIndex = rotateIndex + 1;   // ★ 改为累加,不取模
              img.dataset.rotateIndex = rotateIndex.toString();
              console.log('[右旋] rotateIndex:', rotateIndex);
              // 延迟执行布局调整,等待旋转动画完成
              setTimeout(() => {
                handleImageLayout.applyImageSize(box, rotateIndex);
              }, 50);
            });
          }
        });
      },
      // 监听图片盒子的状态变化
      observeImageBoxes(container) {
        const imgBoxes = container.querySelectorAll('.h-threads-img-box');
        imgBoxes.forEach(imgBox => {
          // ===== 新增:预览框内的图片不观察 =====
          if (imgBox.closest('.h-preview-box')) return;
          // 如果已经激活但没有正确的尺寸,立即处理
          if (imgBox.classList.contains('h-active')) {
            const imgA = imgBox.querySelector('.h-threads-img-a');
            if (imgA) {
              const currentWidth = parseInt(imgA.style.width) || 0;
              const currentHeight = parseInt(imgA.style.height) || 0;
              // 如果容器尺寸异常(太小或为0),强制重新计算
              if (currentWidth < 50 || currentHeight < 50) {
                requestAnimationFrame(() => {
                  if (imgBox.classList.contains('h-active')) {
                    handleImageLayout.handleActiveImageBox(imgBox, true);
                  }
                });
              }
            }
          }
          // 监听 class 变化
          if (!imgBox.__overflowObserver) {
            // ★ 计数埋点:每个带图回复盒一个(单图复杂度),数量应≈带图回复数且不随滚动/翻页重复创建
            startupPerfDebug.record('observer.create:hdImageBox', 0, { tag: 'img-box' });
            const observer = new MutationObserver(mutations => {
              startupPerfDebug.measureObserver('hdImageBox', mutations, () => {
              mutations.forEach(mutation => {
                if (mutation.attributeName === 'class') {
                  handleImageLayout.handleActiveImageBox(imgBox);
                  // ★ 图片收起(h-active 被移除)后补一次首次判定:展开态下首次判定会被跳过,
                  // 而收起时补写容器宽度的旧逻辑(约 9517-9530,注释掉的死代码)缺失,缺这一步该回复将永久不加宽。
                  // 已参与过加宽的回复无需重算:预算已在首次判定时一次算入后续变化的最坏增量,
                  // min-width 语义下收起后内容回落只会让表格回到该宽度,不会突破。
                  // 复用该图片盒已有的 class observer,事件驱动,不做周期性扫描。
                  if (!imgBox.classList.contains('h-active')) {
                    const replyMain = imgBox.closest('.h-threads-item-reply-main');
                    if (replyMain && replyMain.__imageWidthExpanded !== true) {
                      handleImageLayout.expandMsgWidthIfImageExists(replyMain);
                    }
                  }
                }
              });
              }, () => ({ imgBoxes: 1 }));
            });
            observer.observe(imgBox, {
              attributes: true,
              childList: true,
              subtree: true
            });
            imgBox.__overflowObserver = observer;
          }
        });
      }
    };
    function expandReplyMainWidths(scope) {
      const replyMains = [];
      if (scope.matches && scope.matches('.h-threads-item-reply-main')) {
        replyMains.push(scope);
      }
      if (scope.querySelectorAll) {
        scope.querySelectorAll('.h-threads-item-reply-main').forEach(msgMain => {
          replyMains.push(msgMain);
        });
      }
      // ★ 向上取证:pending 标记常落在 reply-main 的后代(.h-threads-img-box / .h-threads-img-a /
      // .h-threads-img / .h-threads-content)上,仅向下查找会漏掉所属 reply-main(该回复永远不加宽)
      const upwardMain = scope.closest && scope.closest('.h-threads-item-reply-main');
      if (upwardMain && upwardMain !== scope) {
        replyMains.push(upwardMain);
      }
      replyMains.forEach(msgMain => {
        handleImageLayout.expandMsgWidthIfImageExists(msgMain);
      });
    }
    // ==================== 执行所有处理 ====================
    // 1. 替换高清链接
    handleImageInteraction.replaceHDLinks(root);
    hdImageLazyLoader.observe(root);
    // 2. 绑定图片点击和控件
    handleImageInteraction.bindImageClickLogic(root);
    handleImageInteraction.bindImageControls(root);
    if (typeof enableImageContextMenu === 'function') enableImageContextMenu(root);
    // 3. 处理普通元素溢出
    handleImageLayout.handleGeneralElements(root);
    // 4. 监听图片盒子状态
    handleImageInteraction.observeImageBoxes(root);
    // ★ 页面加载和无缝翻页新内容都需要拓展带图回复宽度
    // 拓展基数=信息行宽,会被 hideEmptyTitleAndEmail 改变约97px;必须保证同一 root 内先 hide 再量宽,
    // 否则 document级/引用浮窗/pending 等路径读到 hide 前宽度,与 applyPageEnhancements 路径得到两种结果(竞态)
    try {
      const _cfg = getEarlyStartupConfig();
      if (_cfg.hideEmptyTitleEmail && typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail(root);
    } catch (e) {}
    expandReplyMainWidths(root);
    // 注:后续增强(相对时间/饼干标记等)造成的宽度变化已按最坏增量算入加宽预算(getExpandedWidthBudget),
    // 一次写入后不再重算,避免渲染后的反复横向变化。
    // ==================== 全局监听 ====================
    // 监听 DOM 变化
    if (root === document && !enableHDImageAndLayoutFix.__globalObserver) {
      // ★ 计数埋点:全局 DOM 观察器必须恒为 1,不得随增量刷新/翻页重复创建
      startupPerfDebug.record('observer.create:hdGlobal', 0, { tag: 'hd-global' });
      const observer = new MutationObserver(mutations => {
        startupPerfDebug.measureObserver('hdGlobal', mutations, () => {
        mutations.forEach(mutation => {
          mutation.addedNodes.forEach(node => {
            if (node.nodeType !== 1 || !node.dataset) return;
            if (node.closest && node.closest('#sp_panel')) return;
            const isRelevant = node.matches?.('.h-threads-img-box, .h-threads-img-a, .h-threads-img, .h-threads-content, .h-preview-box')
              || node.querySelector?.('.h-threads-img-box, .h-threads-img-a, .h-threads-img, .h-threads-content, .h-preview-box');
            if (isRelevant) node.dataset.xdexHdLayoutPending = '1';
          });
        });
        schedulePendingHDImageAndLayoutFix();
        }, () => startupPerfDebug.summarizeRoot(document));
      });
      observer.observe(document.body, {
        childList: true,
        subtree: true
      });
      enableHDImageAndLayoutFix.__globalObserver = observer;
    }
    // 监听窗口大小改变
    if (root === document && !enableHDImageAndLayoutFix.__resizeHandlerBound) {
      window.addEventListener('resize', () => {
        // 一帧只处理一次:拖动窗口时 resize 每秒可触发数十次,每次都全文档处理图片布局
        const run = () => {
          enableHDImageAndLayoutFix.__resizeFrameId = 0;
          handleImageLayout.handleGeneralElements(document);
          document.querySelectorAll('.h-threads-img-box.h-active').forEach(imgBox => {
            handleImageLayout.handleActiveImageBox(imgBox, true);
          });
          // ★ 补跑全量加宽(QwQNT 时序自愈):初始 pass 可能因 innerWidth 未稳定
          // (QQ↔nmbxd 切换、窗口还原时序)走到 targetWidth<=0 静默返回且不置锁,
          // 该分支无任何兜底;resize 补跑借 __imageWidthExpanded 锁对已加宽者 O(1) 跳过
          try { expandReplyMainWidths(document); } catch (e) {}
        };
        if (enableHDImageAndLayoutFix.__resizeFrameId) return;
        if (typeof requestAnimationFrame !== 'function') { run(); return; }
        enableHDImageAndLayoutFix.__resizeFrameId = requestAnimationFrame(run);
      });
      enableHDImageAndLayoutFix.__resizeHandlerBound = true;
    }
    // ★ 供驻留/可见性链路补跑加宽(QwQNT 时序自愈):expandReplyMainWidths 是本函数内部实现,
    //   外部(visibilitychange 监听器)无法直接访问,挂到函数属性上桥接;
    //   document 级首次 pass 时执行到这里,此后外部即可通过可选链调用
    if (root === document) enableHDImageAndLayoutFix.expandReplyMainWidths = expandReplyMainWidths;
    }, () => startupPerfDebug.summarizeRoot(root));
  }
  function handlePendingHDImageAndLayoutFix(root = document) {
    if (!root || !root.querySelectorAll) return;
    const queue = handlePendingHDImageAndLayoutFix.__queue || [];
    const queued = handlePendingHDImageAndLayoutFix.__queued || new Set();
    handlePendingHDImageAndLayoutFix.__queue = queue;
    handlePendingHDImageAndLayoutFix.__queued = queued;
    root.querySelectorAll('[data-xdex-hd-layout-pending="1"]').forEach(node => {
      delete node.dataset.xdexHdLayoutPending;
      if (queued.has(node)) return;
      queued.add(node);
      queue.push(node);
    });
    handlePendingHDImageAndLayoutFix.__scheduled = false;
    if (handlePendingHDImageAndLayoutFix.__running || !queue.length) return;
    processPendingHDImageAndLayoutFix(root);
  }
  function processPendingHDImageAndLayoutFix(root = document) {
    const queue = handlePendingHDImageAndLayoutFix.__queue || [];
    const queued = handlePendingHDImageAndLayoutFix.__queued || new Set();
    const batchSize = 5;
    handlePendingHDImageAndLayoutFix.__running = true;
    startupPerfDebug.measure('enableHDImageAndLayoutFix.pending', () => {
      let processed = 0;
      while (processed < batchSize && queue.length) {
        const node = queue.shift();
        queued.delete(node);
        if (node && node.isConnected) {
          // ★ 标记常落在 reply-main 的后代(图片盒/锚/图片/正文)上:直接以该后代为 root 会导致
          // 其所属 reply-main 不在作用域内(既不加宽、也绑不上图片控件),故提升到最近的整条回复
          const scope = (node.closest && node.closest('.h-threads-item-reply')) || node;
          enableHDImageAndLayoutFix(scope);
          processed++;
        }
      }
    }, () => startupPerfDebug.summarizeRoot(root));
    if (queue.length) {
      setTimeout(() => processPendingHDImageAndLayoutFix(root), 0);
      return;
    }
    handlePendingHDImageAndLayoutFix.__running = false;
  }
  function schedulePendingHDImageAndLayoutFix() {
    if (handlePendingHDImageAndLayoutFix.__scheduled) return;
    handlePendingHDImageAndLayoutFix.__scheduled = true;
    setTimeout(() => handlePendingHDImageAndLayoutFix(document), 0);
  }
  // 旧逻辑,只作用于预览框
  function enableHDImage(root = document) {
    return startupPerfDebug.measure('enableHDImage', () => {
    // 记录点击次数(用于切换展开/收起)
    const clickCountMap = new WeakMap();
    let lastClickedAnchor = null;
    // 1. 仅在预览框内准备高清链接,实际请求交给近视窗懒加载
    root.querySelectorAll('.h-preview-box img').forEach(img => {
      const src = img.currentSrc || img.src || img.getAttribute('src') || '';
      if (src.includes('/thumb/')) {
        const hdSrc = hdImageLazyLoader.prepare(img, src);
        if (hdImageLazyLoader.isGifUrl(hdSrc)) hdImageLazyLoader.load(img, hdSrc);
      }
    });
    root.querySelectorAll('.h-preview-box a').forEach(a => {
      if (a.href.includes('/thumb/')) {
        a.href = hdImageLazyLoader.getHdUrl(a.href);
      }
    });
    hdImageLazyLoader.observe(root);
    // 2. 仅绑定预览框内的图片盒子的点击逻辑
    root.querySelectorAll('.h-preview-box .h-threads-img-a').forEach(anchor => {
      if (anchor.dataset.hdBound === "1") return;
      anchor.dataset.hdBound = "1";
      anchor.addEventListener('click', function (e) {
        e.preventDefault();
        // 保护:如果不是预览框中的 anchor,直接返回
        if (!this.closest('.h-preview-box')) return;
        const box = this.closest('.h-threads-img-box');
        if (!box) return;
        const img = box.querySelector('.h-threads-img');
        if (!img) return;
        // 计数切换展开/收起
        if (lastClickedAnchor !== this) {
          lastClickedAnchor = this;
          clickCountMap.set(this, 0);
        }
        let count = (clickCountMap.get(this) || 0) + 1;
        clickCountMap.set(this, count);
        if (count % 2 === 1) {
          box.classList.add('h-active');
          hdImageLazyLoader.load(img, this.href);
          updatePreviewImageLayout(box);
        } else {
          box.classList.remove('h-active');
          img.style.transform = '';
          img.style.top = '0px';
          img.style.left = '0px';
          img.dataset.rotateIndex = 0;
          const ds = img.getAttribute('data-src');
          if (ds) img.src = ds;
          else if (img.dataset.xdexThumbSrc) img.src = img.dataset.xdexThumbSrc;
          updatePreviewImageLayout(box);
        }
      });
    });
    // 3. 工具按钮逻辑(收起/旋转)
    const rotateArray = [
      'matrix(1, 0, 0, 1, 0, 0)',
      'matrix(0, 1, -1, 0, 0, 0)',
      'matrix(-1, 0, 0, -1, 0, 0)',
      'matrix(0, -1, 1, 0, 0, 0)'
    ];
    function applyResizeForRotation(img, imgA, rotateIndex) {
      if (!img || !imgA) return;
      const normalizedRotateIndex = ((rotateIndex % rotateArray.length) + rotateArray.length) % rotateArray.length;
      img.style.setProperty('transform', rotateArray[normalizedRotateIndex], 'important');
      img.style.setProperty('transform-origin', 'center center', 'important');
      const width = img.width;
      const height = img.height;
      if (normalizedRotateIndex === 1 || normalizedRotateIndex === 3) {
        const offset = (width - height) / 2;
        img.style.setProperty('top', offset + 'px', 'important');
        img.style.setProperty('left', -offset + 'px', 'important');
        imgA.style.height = width + 'px';
      } else {
        img.style.setProperty('top', '0px', 'important');
        img.style.setProperty('left', '0px', 'important');
        imgA.style.height = height + 'px';
      }
    }
    function updatePreviewImageLayout(box) {
      if (!box) return;
      const img = box.querySelector('.h-threads-img');
      const imgA = box.querySelector('.h-threads-img-a');
      if (!img || !imgA) return;
      const rotateIndex = Number.parseInt(img.dataset.rotateIndex || '0', 10) || 0;
      applyResizeForRotation(img, imgA, rotateIndex);
    }
    function rotatePreviewImage(box, delta) {
      if (!box || !box.closest('.h-preview-box')) return;
      const img = box.querySelector('.h-threads-img');
      const imgA = box.querySelector('.h-threads-img-a');
      if (!img || !imgA) return;
      if (!box.classList.contains('h-active')) {
        box.classList.add('h-active');
        hdImageLazyLoader.load(img, imgA.getAttribute('href') || img.src || '');
      }
      const current = Number.parseInt(img.dataset.rotateIndex || '0', 10) || 0;
      const next = (current + delta + rotateArray.length) % rotateArray.length;
      img.dataset.rotateIndex = String(next);
      updatePreviewImageLayout(box);
      requestAnimationFrame(() => updatePreviewImageLayout(box));
    }
    // 3. 仅绑定预览框内的图片盒子工具按钮
    root.querySelectorAll('.h-preview-box .h-threads-img-box').forEach(box => {
      if (box.dataset.toolBound === "1") return;
      box.dataset.toolBound = "1";
      // 保护:如果该盒子不在预览框,则跳过
      if (!box.closest('.h-preview-box')) return;
      const img = box.querySelector('.h-threads-img');
      const imgA = box.querySelector('.h-threads-img-a');
      const toolSmall = box.querySelector('.h-threads-img-tool-small');
      const toolLarge = box.querySelector('.h-threads-img-tool-large');
      const toolLeft = box.querySelector('.h-threads-img-tool-left');
      const toolRight = box.querySelector('.h-threads-img-tool-right');
      if (toolSmall && imgA) {
        toolSmall.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopImmediatePropagation();
          imgA.click();
        });
      }
      if (toolLarge && imgA) {
        toolLarge.href = imgA.href || img.src || 'javascript:;';
        toolLarge.target = '_blank';
        toolLarge.addEventListener('click', (e) => {
          e.stopImmediatePropagation();
          const url = imgA.href || img.src || '';
          if (!url || url === location.href || /(^|\/)?:javascript:;?$/.test(url)) {
            e.preventDefault();
            return;
          }
          toolLarge.href = url;
        });
      }
      if (toolLeft && img) {
        toolLeft.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopImmediatePropagation();
          rotatePreviewImage(box, -1);
        });
      }
      if (toolRight && img) {
        toolRight.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopImmediatePropagation();
          rotatePreviewImage(box, 1);
        });
      }
    });
    }, () => startupPerfDebug.summarizeRoot(root));
  }
  // 板块页链接新标签页打开
  function runLinkBlank(root = document) {
    const selector = '#h-content .h-threads-list a, .margin-bottom a, .margin-top a, [style*="margin-top: -5px"] a, [style*="margin-top:-5px"] a, #h-menu-content a';
    root.querySelectorAll(selector).forEach(a => {
        // ===== 新增:排除分页导航内的链接 =====
        if (a.closest('ul.uk-pagination.uk-pagination-left.h-pagination')) return;
        if (a.closest('.uk-parent')) return;
        const href = String(a.getAttribute('href') || '').trim();
        if (!href || href === '#' || /^javascript:/i.test(href)) return;
        // =====================================
        a.setAttribute('target', '_blank');
        if (!a.dataset.xdexLinkBlankCurrentBound) {
          a.dataset.xdexLinkBlankCurrentBound = '1';
          a.addEventListener('click', e => {
            if (e.button !== 0 || !(e.ctrlKey || e.metaKey)) return;
            e.preventDefault();
            window.location.href = a.href;
          });
        }
    });
  }

  /* --------------------------------------------------
   * tag 9. 引用浮窗/鼠标离开后自动隐藏原生引用/引用格式拓展
   * -------------------------------------------------- */
  function enableQuotePreview() {
    if (enableQuotePreview.__initialized) return;
    const quotePreviewRoot = document.body;
    if (!quotePreviewRoot) {
      setTimeout(enableQuotePreview, 25);
      return;
    }
    const cache = Object.create(null);
    // 防止短时间内重复点击同一处引用号导致多重弹窗
    let lastQuoteEl = null;
    let lastQuoteAt = 0;
    const QUOTE_DOUBLE_CLICK_WINDOW = 250;
    // 注入样式(只注入一次)
    if (!document.getElementById('qp-styles')) {
      const style = document.createElement('style');
      style.id = 'qp-styles';
      style.textContent = `
        .qp-overlay-quote {
          position: fixed; inset: 0;
          /* 10004:高于导出串弹窗(10001)与设置面板(9999) —— 弹窗内点串号时引用浮窗必须盖在弹窗上 */
          z-index: 10004;
          background: rgba(0,0,0,.45);
          display: none;
        }
        .qp-stack {
          position: fixed;
          top: 55%; /* 下移一点 */
          left: 50%;
          transform: translate(-50%, -50%);
          width: min(90vw, 820px);
          height: 80vh;
          overflow: visible;
          box-sizing: border-box;
        }
        .qp-close-all {
          position: fixed; right: 12px; bottom: 12px;
          width: 30px; height: 30px; padding: 0;
          display: inline-flex; align-items: center; justify-content: center;
          background: var(--xdex-qp-reset-bg, rgba(0,0,0,.6));
          border: 1px solid var(--xdex-qp-reset-border, rgba(255,255,255,.35)); border-radius: 8px; cursor: pointer; z-index: 10005; /* 高于 .qp-overlay-quote(10004) */
          user-select: none;
          color: var(--xdex-qp-reset-color, #fff);
        }
        .qp-close-all:hover {
          border-color: #EE0000 !important;
          color: #EE0000 !important;
        }
        /* 两个刷新钮(归位/局部刷新):悬浮边框与 svg 图标同步变绿 */
        .qp-reset-btn.xdex-icon-btn:hover,
        .seamless-refresh-btn.xdex-icon-btn:hover {
          border-color: #00ffcc !important;
          color: #00ffcc !important;
        }
        .qp-reset-btn.xdex-icon-btn:hover svg,
        .seamless-refresh-btn.xdex-icon-btn:hover svg {
          stroke: #00ffcc !important;
        }
        .qp-overlay-quote .qp-quote {
          position: absolute;
          top: 0; left: 0;
          width: 100%; max-height: 100%;
          overflow: auto;
          /* 走 darkreader 兼容 token,避免写死 #FFFFEE */
          background: var(--xdex-qp-shell-bg, #FFFFEE);
          border: 1px solid var(--xdex-qp-border, #ccc);
          outline: 2px solid var(--xdex-qp-outline, #fff);
          border-radius: 8px;
          box-shadow: 0 8px 24px var(--xdex-qp-shadow, rgba(0,0,0,.24));
          padding: 18px 20px 20px;
          box-sizing: border-box;
        }
        .qp-quote * { max-width: 100%; box-sizing: border-box; }
        .qp-header {
          position: sticky; top: 0;
          display: flex; gap: 10px; justify-content: flex-end;
          padding-bottom: 6px; margin: -8px -8px 10px; padding: 8px;
          background: transparent; /* ✅ 改为透明背景 */
          z-index: 2;
        }
        .qp-overlay-quote .qp-level {
          font-size: 12px; color: inherit; background: color-mix(in srgb, var(--xdex-qp-border, #ccc) 35%, transparent); border-radius: 4px; padding: 2px 6px;
        }
        .qp-overlay-quote .qp-back {
          font-size: 12px; color: inherit; background: color-mix(in srgb, var(--xdex-qp-border, #ccc) 22%, transparent);
          border: 1px solid var(--xdex-qp-border, #ccc); border-radius: 4px; padding: 2px 6px;
          cursor: pointer;
        }
        .qp-overlay-quote .qp-jump {
          font-size: 12px; color: inherit; background: color-mix(in srgb, var(--xdex-qp-border, #ccc) 22%, transparent);
          border: 1px solid var(--xdex-qp-border, #ccc); border-radius: 4px; padding: 2px 6px;
          cursor: pointer;
        }
        .qp-overlay-quote .qp-quote.is-dragging,
        .qp-overlay-quote .qp-quote.is-resizing { cursor: grabbing !important; user-select: none !important; }
        /* 引用浮窗四角拉伸(与回复浮窗一致:角=缩放,边=拖动) */
        .qp-overlay-quote .qp-resize-corner {
          position: absolute;
          width: 14px;
          height: 14px;
          z-index: 5;
          pointer-events: auto;
          background: transparent;
        }
        .qp-overlay-quote .qp-resize-corner.nw { top: 0; left: 0; cursor: nwse-resize; }
        .qp-overlay-quote .qp-resize-corner.ne { top: 0; right: 0; cursor: nesw-resize; }
        .qp-overlay-quote .qp-resize-corner.sw { bottom: 0; left: 0; cursor: nesw-resize; }
        .qp-overlay-quote .qp-resize-corner.se { bottom: 0; right: 0; cursor: nwse-resize; }
        .qp-overlay-quote .qp-drag-edge.top    { top: 0;    left: 14px; right: 14px; height: 10px; }
        .qp-overlay-quote .qp-drag-edge.bottom { bottom: 0; left: 14px; right: 14px; height: 10px; }
        .qp-overlay-quote .qp-drag-edge.left   { top: 14px; bottom: 14px; left: 0;   width: 10px; }
        .qp-overlay-quote .qp-drag-edge.right  { top: 14px; bottom: 14px; right: 0;  width: 10px; }
        #h-ref-view { pointer-events: none !important; }
        #h-ref-view {
          z-index: 20000 !important; /* 保证原生引用框在浮窗之上 */
        }
        /* 拖拽手柄:四边窄条,仅在手柄区域显示“移动”指针 */
        .qp-drag-edge {
          position: absolute;
          pointer-events: auto;
          z-index: 3;
          cursor: move;
        }
        .qp-drag-edge.top    { top: 0;    left: 0;    right: 0;  height: 10px; }
        .qp-drag-edge.bottom { bottom: 0; left: 0;    right: 0;  height: 10px; }
        .qp-drag-edge.left   { top: 0;    bottom: 0;  left: 0;   width: 10px; }
        .qp-drag-edge.right  { top: 0;    bottom: 0;  right: 0;  width: 10px; } 
        /* 标题栏作为拖拽手柄时的指针反馈 */
        .qp-header { cursor: move; }
      `;
      document.head.appendChild(style);
    }
    //✅ 监听原生引用浮窗的显示,如果鼠标不在引用号上则立即隐藏
    const observer = new MutationObserver(() => {
      const refView = document.getElementById('h-ref-view');
      if (!refView) return;
      const display = refView.style.display;
      // ✅ 当浮窗被隐藏时(none),重置透明度
      if (display === 'none') {
        refView.style.opacity = '';
        return;
      }
      // ✅ 当浮窗显示时(block),检查鼠标是否在引用号上
      if (display === 'block') {
        const quoteFonts = document.querySelectorAll('font[color="#789922"]');
        let isHovering = false;
        quoteFonts.forEach(font => {
          if (font.matches(':hover')) {
            isHovering = true;
          }
        });
        // ✅ 显示但鼠标不在引用号上 → 立即隐藏并重置透明度
        if (!isHovering) {
          refView.style.display = 'none';
          refView.style.opacity = '';
        }
      }
    });
    observer.observe(quotePreviewRoot, { 
      childList: true, 
      subtree: true,
      attributes: true,
      attributeFilter: ['style']
    });
    const $overlay = $('<div class="qp-overlay-quote"></div>').appendTo('body');
    const $stack   = $('<div class="qp-stack"></div>').appendTo($overlay);
    const $closeAll= $('<div class="qp-close-all" title="关闭所有引用浮窗"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="red" stroke-width="3" stroke-linecap="round" aria-hidden="true"><path style="fill:none" d="M6 6l12 12"/><path style="fill:none" d="M18 6L6 18"/></svg></div>').appendTo($overlay);
    $closeAll.on('click', () => {
      $stack.children('.qp-quote').each((_, q) => clearQuoteSourceMark($(q)));
      $stack.empty();
      $overlay.fadeOut(160);
    });
    // 点击引用框以外的任何地方都关闭
    $overlay.off('click.qp').on('click.qp', function(e){
      if (!$overlay.is(':visible')) return;
      const $top = $stack.children('.qp-quote').last();
      if (!$top.length) { $overlay.fadeOut(160); return; }
      // 1) 如果点击发生在最上层引用框内部,忽略
      //    必须用派发时定格的 composedPath 判定:冒泡途中控件若替换了自身内容,e.target 会脱离文档,
      //    closest($top) 随即失效、被误判成「点了框外」而误关引用浮窗(jQuery 事件需取 originalEvent)
      const nativeEv = e.originalEvent || e;
      const evPath = typeof nativeEv.composedPath === 'function' ? nativeEv.composedPath() : null;
      const inTopQuote = evPath
          ? evPath.indexOf($top[0]) !== -1
          : !!(e.target && e.target.closest && e.target.closest($top[0]));
      if (inTopQuote) return;
      // 2) 如果正在/刚刚拖拽或拉伸,避免误触关闭
      if ($top.hasClass('is-dragging') || $top.hasClass('is-resizing') ||
          $('.qp-quote.is-dragging').length || $('.qp-quote.is-resizing').length ||
          $overlay.data('isDragging')) return;
      // 3) 点击最上层框之外:仅移除最上层
      clearQuoteSourceMark($top);
      $top.remove();
      if ($stack.children('.qp-quote').length === 0) $overlay.fadeOut(160);
    });
    $(document).on('keydown.qp', e => {
      if (e.key !== 'Escape' || !$overlay.is(':visible')) return;
      const $last = $stack.children('.qp-quote').last();
      if ($last.length) { clearQuoteSourceMark($last); $last.remove(); }
      if ($stack.children().length === 0) $overlay.fadeOut(160);
    });
        // 浮窗关闭时清除源引用元素上的“已打开”标记,允许之后再次点击打开
    function clearQuoteSourceMark($quote) {
      try {
        const el = $quote && $quote[0] && $quote[0].__xdexSourceEl;
        if (el) {
          el.__xdexQuoteOpen = false;
          el.__xdexQuotePending = false;
        }
      } catch (e) {}
    }
    function fetchData(tid) {
      if (cache[tid]) return Promise.resolve(cache[tid]);
      return $.get(`/Home/Forum/ref?id=${tid}`).then(html => (cache[tid] = html));
    }
    function stripIds($root) {
      $root.find('[id]').removeAttr('id');
      return $root;
    }
    function simplifyQuoteInfoIdLinks($root) {
      if (!$root || !$root.find) return;
      $root.find('a.h-threads-info-id[href]').each(function () {
        const a = this;
        const href = a.getAttribute('href') || '';
        if (!href) return;
        let url;
        try {
          url = new URL(href, location.origin);
        } catch (e) {
          return;
        }
        // 仅处理 /t/{tid}?r={...} 这类链接
        const m = url.pathname.match(/^\/t\/(\d{4,})$/);
        if (!m) return;
        const tid = m[1];
        const rid = url.searchParams.get('r');
        // 仅当 r 与 tid 完全一致时精简为 /t/{tid}
        if (rid && rid === tid) {
          a.setAttribute('href', `/t/${tid}`);
        }
      });
    }
    function showQuote(html, options = {}) {
      const depth = $stack.children('.qp-quote').length;
      const $quote = $('<div class="qp-quote"></div>').css({
        top: '0px',
        left: '0px',
        zIndex: 1000 + depth
      });
      // 关联源引用元素:浮窗关闭时据此清除其“已打开”标记
      if (options && options.sourceEl) $quote[0].__xdexSourceEl = options.sourceEl;
      const $header = $('<div class="qp-header"></div>');
      const $level  = $(`<span class="qp-level">第 ${depth + 1} 层</span>`);
      const $back   = $('<button class="qp-back">返回</button>').on('click', e => {
        e.stopPropagation();
        clearQuoteSourceMark($quote);
        $quote.remove();
        if ($stack.children().length === 0) $overlay.fadeOut(160);
      });
      // 跳转按钮:引用号被判为 thread(主串) 时插入到“第 N 层”左侧(不限层数)
      let $jump = null;
      const refTid = options && options.tid ? String(options.tid).trim() : '';
      const ensureJumpButton = () => {
        if (!refTid || $jump) return;
        const kind = quoteAvailabilityCache && quoteAvailabilityCache[refTid] && quoteAvailabilityCache[refTid].kind;
        if (kind !== 'thread') return;
        $jump = $('<button class="qp-jump">跳转</button>').on('click', e => {
          e.stopPropagation();
          let url = '';
          try { url = getLatestThreadHistoryUrl(refTid) || `${location.origin}/t/${refTid}`; } catch (err) { url = `${location.origin}/t/${refTid}`; }
          window.open(url, '_blank');
        });
        $level.before($jump);
      };
      // 判定为 thread 时补插:通过全局监听注册表触发,成功即注销
      const onThreadAvail = (tid) => {
        if (String(tid) === refTid) {
          ensureJumpButton();
          offQuoteAvailThread(refTid, onThreadAvail);
        }
      };
      $header.append($level, $back);
      ensureJumpButton();
      if (refTid) onQuoteAvailThread(refTid, onThreadAvail);
      $quote.append($header);
      const $content = stripIds($('<div></div>').html(html));
      simplifyQuoteInfoIdLinks($content);
      // 引用浮窗内容同样应用“引用号是否当前串”标注:currentThreadId 由打开方传入
      if (options && options.currentThreadId) {
        try { markCurrentThreadQuoteRefs($content[0], options.currentThreadId); } catch (e) {}
      }
      $quote.append($content.contents());
      // 在 $quote 内添加四条边框拖拽手柄 + 四角拉伸手柄

      const $edges = $(
        '<div class="qp-drag-edge top"></div>' +
        '<div class="qp-drag-edge bottom"></div>' +
        '<div class="qp-drag-edge left"></div>' +
        '<div class="qp-drag-edge right"></div>' +
        '<div class="qp-resize-corner nw" data-dir="nw"></div>' +
        '<div class="qp-resize-corner ne" data-dir="ne"></div>' +
        '<div class="qp-resize-corner sw" data-dir="sw"></div>' +
        '<div class="qp-resize-corner se" data-dir="se"></div>'
      );
      $quote.append($edges);
      // 仅在标题栏 + 四边手柄上触发拖拽
      enableDragForTop($quote, $quote.find('.qp-header, .qp-drag-edge'));
      // 四角拉伸(与回复浮窗一致)
      enableResizeForQuote($quote);
      $stack.append($quote);
      $overlay.fadeIn(160);
      if (typeof syncQuotePopupTheme === 'function') syncQuotePopupTheme();
      enableHDImageAndLayoutFix($quote[0]);
      if (typeof extendQuote === 'function') extendQuote($quote[0]);
      if (typeof initExtendedContent === 'function') initExtendedContent($quote[0]);
      runAutoUrlLinkify($quote[0]);
      runLinkBlank($quote[0]);
      enableHDImageAndLayoutFix();
      hideEmptyTitleAndEmail($quote[0]);
      initContent($quote[0]);
      try {
        const cfg = typeof getFilterConfig === 'function' ? getFilterConfig() : Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
        markAllCookies(cfg.markedGroups || [], $quote[0]);
        if (cfg.enableImageHideMode) applyImageHideMode(cfg.applyImageHideMode || 'default', $quote[0]);
      } catch (e) {}
      if (options && options.fromPOImage) {
        setTimeout(() => {
          try {
            const imgAnchor = $quote[0].querySelector('.h-threads-img-box .h-threads-img-a');
            if (!imgAnchor) return;
            const imgBox = imgAnchor.closest('.h-threads-img-box');
            if (!imgBox || imgBox.classList.contains('h-active')) return;
            imgAnchor.click();
          } catch (e) {}
        }, 0);
      }
      //autoHideRefView();
    }
    window.__xdexOpenQuoteByTid = function(tid, options = {}) {
      if (!tid) return Promise.resolve(false);
      return fetchData(String(tid)).then(html => {
        // 引用浮窗“当前串”上下文:显式传入优先,否则用调用方临时写入的上下文
        if (!options.currentThreadId && window.__xdexPendingQuoteCtxTid) {
          options.currentThreadId = window.__xdexPendingQuoteCtxTid;
          window.__xdexPendingQuoteCtxTid = '';
        }
        options.tid = String(tid);
        showQuote(html, options);
        return true;
      }).catch(err => {
        console.warn('open quote by tid failed', tid, err);
        return false;
      });
    };
    function enableDragForTop($quote, $handles) {
      // 与四角缩放一致:仅在本次 mousedown 手势内挂 window 监听,mouseup 卸掉。
      // 避免「后开层 off 掉全局 qpdrag → 关层后下层只剩 mousedown、拖不动」。
      $handles.off('mousedown.qpdrag');
      const top0 = parseInt($quote.css('top'), 10) || 0;
      const left0 = parseInt($quote.css('left'), 10) || 0;
      $quote.css({
        top: top0 + 'px',
        left: left0 + 'px',
        position: 'absolute'
      });
      $handles.on('mousedown.qpdrag', function(e){
        if (e.button != null && e.button !== 0) return;
        // 避免点击标题栏中的交互控件(返回按钮等)触发拖拽
        if ($(e.target).closest('a,button,input,textarea,select,label').length) return;
        e.preventDefault();
        e.stopPropagation();
        let dx = 0;
        let dy = 0;
        $quote.data('dragging', true);
        $overlay.data('isDragging', true);
        $quote.addClass('is-dragging');
        const currentTop = parseInt($quote.css('top'), 10) || 0;
        const currentLeft = parseInt($quote.css('left'), 10) || 0;
        const stackOff = $stack.offset() || { left: 0, top: 0 };
        dx = e.pageX - currentLeft - stackOff.left;
        dy = e.pageY - currentTop - stackOff.top;
        const onMove = function(ev){
          if (!$quote.data('dragging')) return;
          if (!$quote[0] || !$quote[0].isConnected) {
            onUp(ev);
            return;
          }
          ev.preventDefault();
          const off = $stack.offset() || { left: 0, top: 0 };
          const stackWidth = $stack.width();
          const stackHeight = $stack.height();
          const quoteWidth = $quote.outerWidth();
          const quoteHeight = $quote.outerHeight();
          let top = ev.pageY - dy - off.top;
          let left = ev.pageX - dx - off.left;
          // 限制拖拽范围,允许向左和向上拖出一部分(至少保留 50px 可见)
          top = Math.max(-quoteHeight + 50, Math.min(stackHeight - 50, top));
          left = Math.max(-quoteWidth + 50, Math.min(stackWidth - 50, left));
          $quote.css({ top: top + 'px', left: left + 'px' });
        };
        const onUp = function(){
          window.removeEventListener('mousemove', onMove, true);
          window.removeEventListener('mouseup', onUp, true);
          if (!$quote.data('dragging')) return;
          $quote.data('dragging', false);
          $quote.removeClass('is-dragging');
          // 延迟清除,避免释放瞬间点到遮罩误关
          setTimeout(() => {
            $overlay.data('isDragging', false);
          }, 100);
        };
        window.addEventListener('mousemove', onMove, true);
        window.addEventListener('mouseup', onUp, true);
      });
    }
    // 引用浮窗四角拉伸:对角锚定,与回复浮窗交互一致
    function enableResizeForQuote($quote) {
      const QUOTE_MIN_W = 280;
      const QUOTE_MIN_H = 160;
      $quote.find('.qp-resize-corner').off('mousedown.qpresize').on('mousedown.qpresize', function (e) {
        if (e.button !== 0) return;
        const dir = this.getAttribute('data-dir') || '';
        if (!dir) return;
        e.preventDefault();
        e.stopPropagation();
        const startX = e.clientX;
        const startY = e.clientY;
        const startRect = $quote[0].getBoundingClientRect();
        const startTop = parseFloat($quote.css('top')) || 0;
        const startLeft = parseFloat($quote.css('left')) || 0;
        const startW = startRect.width;
        const startH = startRect.height;
        $quote.addClass('is-resizing');
        $overlay.data('isDragging', true); // 避免缩放结束点到遮罩误关
        const onMove = (ev) => {
          ev.preventDefault();
          const dx = ev.clientX - startX;
          const dy = ev.clientY - startY;
          let w = startW;
          let h = startH;
          let top = startTop;
          let left = startLeft;
          if (dir.indexOf('e') !== -1) w = startW + dx;
          if (dir.indexOf('s') !== -1) h = startH + dy;
          if (dir.indexOf('w') !== -1) {
            w = startW - dx;
            left = startLeft + dx;
          }
          if (dir.indexOf('n') !== -1) {
            h = startH - dy;
            top = startTop + dy;
          }
          if (w < QUOTE_MIN_W) {
            if (dir.indexOf('w') !== -1) left = startLeft + (startW - QUOTE_MIN_W);
            w = QUOTE_MIN_W;
          }
          if (h < QUOTE_MIN_H) {
            if (dir.indexOf('n') !== -1) top = startTop + (startH - QUOTE_MIN_H);
            h = QUOTE_MIN_H;
          }
          const maxW = window.innerWidth;
          const maxH = window.innerHeight;
          if (w > maxW) w = maxW;
          if (h > maxH) h = maxH;
          $quote.css({
            width: w + 'px',
            height: h + 'px',
            maxHeight: 'none',
            top: top + 'px',
            left: left + 'px'
          });
        };
        const onUp = () => {
          $quote.removeClass('is-resizing');
          // 在捕获阶段拦截紧随 mouseup 的 click,防止误触关闭引用浮窗
          const overlayEl = $overlay[0];
          const eatClick = (ev) => {
            ev.stopPropagation();
            overlayEl.removeEventListener('click', eatClick, true);
          };
          overlayEl.addEventListener('click', eatClick, true);
          setTimeout(() => {
            $overlay.data('isDragging', false);
            overlayEl.removeEventListener('click', eatClick, true); // 兜底清理
          }, 200);
          window.removeEventListener('mousemove', onMove);
          window.removeEventListener('mouseup', onUp);
        };
        window.addEventListener('mousemove', onMove);
        window.addEventListener('mouseup', onUp);
      });
    }
    $(document).off('click.qp').on('click.qp', 'font[color="#789922"]', function(e){
      $('#h-ref-view').hide().css('opacity', '');   // 点击时关闭原生引用框并重置透明度
      e.preventDefault();
      e.stopPropagation();
      const tid = (this.textContent.match(/\d+/) || [])[0];
      if (!tid) return;
      const now = Date.now();
      const sourceEl = this;
      if (lastQuoteEl === sourceEl
          && (now - lastQuoteAt <= QUOTE_DOUBLE_CLICK_WINDOW
              || sourceEl.__xdexQuotePending
              || sourceEl.__xdexQuoteOpen)) {
        return; // 同一处引用号:请求中/浮窗已开/短时间内重复点击,忽略
      }
      lastQuoteEl = sourceEl;
      lastQuoteAt = now;
      sourceEl.__xdexQuotePending = true;
      sourceEl.__xdexQuoteOpen = false;
      const ctxTid = getRefContextThreadId(this);
      window.__xdexNativeRefCtxTid = ctxTid;
      const refViewEl = document.getElementById('h-ref-view');
      if (refViewEl) {
        try { markCurrentThreadQuoteRefs(refViewEl, ctxTid); } catch (e) {}
      }
      fetchData(tid).then(html => {
        if (sourceEl.__xdexQuotePending) sourceEl.__xdexQuotePending = false;
        sourceEl.__xdexQuoteOpen = true;
        // 与悬浮同源:点击拿到的 ref 原文同样可用于实时定论/优先判定
        if (!settleQuoteRefAsEmpty(tid, html)) prioritizeQuoteProbe(tid);
        showQuote(html, { currentThreadId: ctxTid, tid: String(tid), sourceEl: sourceEl });
        // 兑底:对最上层拓展浮窗内容再标一次,防 options 链路/后处理导致漏标
        setTimeout(() => {
          try {
            const quotes = document.querySelectorAll('.qp-overlay-quote .qp-quote');
            const top = quotes[quotes.length - 1];
            if (top) {
              markCurrentThreadQuoteRefs(top, ctxTid);
              try { refreshQuoteAvailability(top); } catch (e) {}
            }
            console.log('[xdex] quote overlay marked', { ctx: ctxTid, ref: tid });
          } catch (e) {}
        }, 60);
      }).catch(() => {
        // 加载失败:清除 pending,允许之后重试
        sourceEl.__xdexQuotePending = false;
      });
    });
    // 原生引用浮窗标注(多路兑底):ctx 来自最近一次 hover/click 的引用号所属串;
    // 每次都重新 getElementById,防止 X岛 重建 #h-ref-view 元素后旧 observer/引用失效
    let nativeRefMarkLast = 0;
    let nativeRefViewDirty = true; // 浮窗内容/上下文变化后需重新标注;稳态下跳过全量扫描
    const markNativeRefViewCurrentThread = (force, ctxOverride) => {
      try {
        const refViewEl = document.getElementById('h-ref-view');
        if (!refViewEl) return;
        const ctx = String(ctxOverride || window.__xdexNativeRefCtxTid || '');
        if (!ctx) return;
        // 稳态短路:上下文未变且浮窗内容未变 → 不做任何查询
        if (!force && !nativeRefViewDirty && refViewEl.dataset.xdexMarkedCtx === ctx) return;
        // 非 force 时跳过隐藏态(显示瞬间由 observer/延迟补标接管);force 用于 hover 后异步填充的补标
        if (!force && refViewEl.style.display === 'none') return;
        markCurrentThreadQuoteRefs(refViewEl, ctx);
        refViewEl.dataset.xdexMarkedCtx = ctx;
        nativeRefViewDirty = false;
      } catch (e) {}
    };
    const ensureNativeRefViewMarkObserver = () => {
      const refViewEl = document.getElementById('h-ref-view');
      if (!refViewEl || refViewEl.__xdexRefViewMarkObserved) return;
      refViewEl.__xdexRefViewMarkObserved = true;
      const timer = { id: 0 };
      const mark = () => {
        try {
          const ctx = window.__xdexNativeRefCtxTid || '';
          if (!ctx) return;
          markCurrentThreadQuoteRefs(refViewEl, ctx);
        } catch (e) {}
      };
      const observer = new MutationObserver(() => {
        clearTimeout(timer.id);
        timer.id = setTimeout(mark, 120);
      });
      // 监听 style:X岛 将浮窗从 display:none 切换为可见的瞬间也需要标注
      observer.observe(refViewEl, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['style'] });
      mark();
    };
    // 原生 capture 阶段监听:不受 X岛 自身事件 stopPropagation / jQuery 委托时序影响,首次悬浮即能记录上下文
    document.addEventListener('mouseover', function (e) {
      // 快速短路:绝大多数元素非引用号,一次属性比较即返回
      if (!e.target || e.target.tagName !== 'FONT') return;
      if ((e.target.getAttribute('color') || '') !== '#789922') return;
      const ctx = getRefContextThreadId(e.target);
      if (ctx !== window.__xdexNativeRefCtxTid) nativeRefViewDirty = true; // 上下文变了需重标
      window.__xdexNativeRefCtxTid = ctx;
      ensureNativeRefViewMarkObserver();
      markNativeRefViewCurrentThread(true, ctx);
      // 首次悬停时序兑底:X岛 异步填充内容并显示后才能标注,
      // 每 300ms 重试直到浮窗内容出现标记(最多 3 次),覆盖任意填充/显示时序
      let tries = 0;
      const retryUntilMarked = function () {
        tries += 1;
        try {
          const ctx2 = window.__xdexNativeRefCtxTid || '';
          const refViewEl = document.getElementById('h-ref-view');
          if (!ctx2 || !refViewEl) return;
          markCurrentThreadQuoteRefs(refViewEl, ctx2);
          if (!refViewEl.querySelector('font[data-xdex-cur-thread-marked-tid]') && tries < 3) {
            setTimeout(retryUntilMarked, 300);
          }
        } catch (e) {}
      };
      setTimeout(retryUntilMarked, 300);
    }, true);
    $(document).on('mouseleave', 'font[color="#789922"]', function () {
      $('#h-ref-view').hide();   // 鼠标移开时关闭原生引用框
    });
    // 全局监听多个事件,确保原生引用浮窗及时隐藏
    $(document).on('mousemove.refview scroll.refview wheel.refview', function(e) {
      const refView = document.getElementById('h-ref-view');
      if (!refView || refView.style.display === 'none') return;
      // scroll/wheel 事件可能没有有效坐标,直接隐藏更稳妥
      if (e.type === 'scroll' || e.type === 'wheel') {
        refView.style.display = 'none';
        refView.style.opacity = '';
        return;
      }
      const x = Number(e.clientX);
      const y = Number(e.clientY);
      if (!Number.isFinite(x) || !Number.isFinite(y)) {
        return;
      }
      if (x < 0 || y < 0 || x > window.innerWidth || y > window.innerHeight) {
        return;
      }
      // 检查鼠标当前位置下的元素是否是引用号
      let elementsUnderMouse = [];
      try {
        elementsUnderMouse = document.elementsFromPoint(x, y) || [];
      } catch (_) {
        return;
      }
      const quoteFont = elementsUnderMouse.find(el => el.tagName === 'FONT' && el.getAttribute('color') === '#789922');
      const isOnQuote = !!quoteFont;
      // 如果不在引用号上,立即隐藏
      if (!isOnQuote) {
        refView.style.display = 'none';
        refView.style.opacity = '';  // 重置透明度
        return;
      }
      // 即时计算触发上下文:串内页(含只看PO)直接读网址;版块/时间线用鼠标下引用号所属串
      // 并对原生浮窗内容标注(兑底 mouseover/observer 链路失效的场景);稳态下内部 dirty 短路零查询
      const pageTid = (typeof PageType !== 'undefined' && PageType.getThreadId) ? PageType.getThreadId(true) : '';
      const liveCtxTid = pageTid || getRefContextThreadId(quoteFont);
      if (liveCtxTid) window.__xdexNativeRefCtxTid = liveCtxTid;
      const nowMarkTs = Date.now();
      if (nowMarkTs - nativeRefMarkLast > 250) {
        nativeRefMarkLast = nowMarkTs;
        markNativeRefViewCurrentThread(false, liveCtxTid);
      }
    });
    enableQuotePreview.__initialized = true;
  }
  function monitorRefView(){
    const refView = document.getElementById('h-ref-view');
    if (!refView) return;
    if (refView.dataset.exMonitorBound === '1') return;
    refView.dataset.exMonitorBound = '1';
    let rafLock = 0;
    const observer = new MutationObserver(() => {
      if (refView.style.display !== 'block') return;
      if (rafLock) return;
      rafLock = requestAnimationFrame(() => {
        rafLock = 0;
        hideEmptyTitleAndEmail(refView);
        // 原生引用浮窗显示/内容更新时:按最近触发串上下文标注当前串引用号
        try {
          const ctx = window.__xdexNativeRefCtxTid || '';
          if (ctx && refView.style.display !== 'none') markCurrentThreadQuoteRefs(refView, ctx);
        } catch (e) {}
      });
    });
    observer.observe(refView, {
      attributes: true,
      attributeFilter: ['style'],
      childList: true,
      subtree: true
    });
  }
  function ensureRefViewLayoutStyle() {
    if (document.getElementById('h-ref-view-layout-style')) return;
    const style = document.createElement('style');
    style.id = 'h-ref-view-layout-style';
    // 有图加宽改走 class 开关,避免 :has(img) 在隐藏图/残留图时误伤无图引用
    style.textContent = `
      #h-ref-view {
        box-sizing: border-box !important;
        max-width: calc(100vw - 2rem) !important;
        overflow: auto !important;
        overflow-wrap: break-word !important;
        word-break: break-word !important;
      }
      #h-ref-view.xdex-ref-view-has-image {
        min-width: min(52rem, calc(100vw - 2rem)) !important;
      }
      #h-ref-view.xdex-ref-view-has-image .h-threads-item-reply-main,
      #h-ref-view.xdex-ref-view-has-image .h-threads-content {
        min-width: 22rem !important;
      }
      #h-ref-view.xdex-ref-view-stack-image.xdex-ref-view-has-image {
        min-width: 0 !important;
        overflow-x: hidden !important;
        overflow-y: auto !important;
      }
      #h-ref-view.xdex-ref-view-stack-image.xdex-ref-view-has-image .h-threads-img-box {
        float: none !important;
        display: block !important;
        max-width: 100% !important;
      }
      #h-ref-view.xdex-ref-view-stack-image.xdex-ref-view-has-image .h-threads-img-a,
      #h-ref-view.xdex-ref-view-stack-image.xdex-ref-view-has-image img {
        max-width: 100% !important;
      }
      #h-ref-view.xdex-ref-view-stack-image.xdex-ref-view-has-image .h-threads-img {
        float: none !important;
        display: block !important;
        margin: 0 0 12px 0 !important;
      }
      #h-ref-view.xdex-ref-view-stack-image.xdex-ref-view-has-image .h-threads-item-reply-main,
      #h-ref-view.xdex-ref-view-stack-image.xdex-ref-view-has-image .h-threads-content {
        clear: both !important;
        display: block !important;
        min-width: 0 !important;
        width: auto !important;
      }
      #h-ref-view img {
        max-width: min(100%, 30rem) !important;
        max-height: 52vh !important;
        width: auto !important;
        height: auto !important;
        object-fit: contain !important;
      }
    `;
    document.head.appendChild(style);
  }
  // 单例 #h-ref-view 会残留上一次有图布局的 width/class,换引用时必须先复位
  function resetRefViewLayoutState(refView) {
    if (!refView) return;
    refView.classList.remove('xdex-ref-view-stack-image', 'xdex-ref-view-has-image');
    refView.style.width = '';
    refView.style.minWidth = '';
    refView.style.maxWidth = '';
  }
  function updateRefViewImageLayout(refView, anchorEl) {
    if (!refView || !anchorEl) return;
    resetRefViewLayoutState(refView);
    const layoutImages = Array.from(refView.querySelectorAll('img')).filter(img => {
      if (img.closest('.xdex-hide-noimage')) return false;
      const box = img.closest('.h-threads-img-box');
      if (box && getComputedStyle(box).display === 'none') return false;
      return getComputedStyle(img).display !== 'none';
    });
    // 无可见图:保持复位后的自然宽度,避免沿用上一条有图引用的加宽
    if (!layoutImages.length) return;
    refView.classList.add('xdex-ref-view-has-image');
    const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
    const imageWidth = 30 * rootFontSize;
    const readableTextWidth = 22 * rootFontSize;
    const stackedReadableWidth = 38 * rootFontSize;
    const margin = 16;
    const viewportWidth = document.documentElement.clientWidth || window.innerWidth || 0;
    const viewportLeft = window.pageXOffset || document.documentElement.scrollLeft || 0;
    const viewportRight = viewportLeft + viewportWidth - margin;
    const anchorLeft = $(anchorEl).offset().left;
    const fullInlineWidth = Math.min(imageWidth + readableTextWidth, Math.max(0, viewportWidth - margin * 2));
    const availableRight = viewportRight - anchorLeft;
    const shouldStack = availableRight < fullInlineWidth;
    const expectedWidth = shouldStack ? Math.min(Math.max(imageWidth, stackedReadableWidth), Math.max(0, viewportWidth - margin * 2)) : fullInlineWidth;
    const left = Math.max(viewportLeft + margin, Math.min(anchorLeft, viewportRight - expectedWidth));
    refView.classList.toggle('xdex-ref-view-stack-image', shouldStack);
    if (shouldStack) {
      layoutImages.forEach(img => {
        img.removeAttribute('align');
        img.removeAttribute('hspace');
      });
    }
    refView.style.left = left + 'px';
    refView.style.width = expectedWidth > 0 ? expectedWidth + 'px' : '';
  }
  function renderHiddenTextContent(root) {
    const $root = $(root || document);
    $root.find('.h-threads-content').add($root.filter('.h-threads-content')).each(function () {
      if (this.dataset.xdexPreviewSpoilerRendered === '1') return;
      const $content = $(this);
      let html = $content.html();
      const hideenRegExp = /\[h\]([\s\S]*?)\[\/h\]/g;
      if (hideenRegExp.test(html)) {
        html = html.replace(hideenRegExp, '<span class="h-hidden-text">$1</span>');
        $content.html(html);
      }
    });
  }
  // function autoHideRefView() {
  //     setInterval(() => {
  //         const refView = document.getElementById('h-ref-view');
  //         if (!refView || getComputedStyle(refView).display === 'none') return;

  //         // 获取当前引用框中的引用号 ID
  //         const infoId = refView.querySelector('.h-threads-info-id');
  //         if (!infoId) return;

  //         const tidMatch = infoId.textContent.match(/\d+/);
  //         if (!tidMatch) return;

  //         const tid = tidMatch[0];

  //         // 查找所有引用号元素
  //         const quoteFonts = document.querySelectorAll("font[color='#789922']");

  //         let isHovering = false;
  //         quoteFonts.forEach(font => {
  //             const text = font.textContent;
  //             if (text.includes(tid) && font.matches(':hover')) {
  //                 isHovering = true;
  //             }
  //         });

  //         // 如果没有任何引用号处于 hover 状态 → 隐藏引用框
  //         if (!isHovering) {
  //             refView.style.display = 'none';
  //         }
  //     }, 300); // 每 300ms 检查一次
  // }

  // —— 引用号“是否当前串”标注(tag 9 扩展):当前串判定 = 引用所在串节点 data-threads-id,串内页(含只看PO)回退 URL ——
  const QUOTE_REF_COLOR = '#789922'; // 与原生引用号颜色一致
  function getRefContextThreadId(el) {
    try {
      if (el && el.closest) {
        const node = el.closest('.h-threads-item[data-threads-id]');
        const tid = node && /^\d{1,}$/.test(node.getAttribute('data-threads-id') || '') ? node.getAttribute('data-threads-id').trim() : '';
        if (tid && tid !== '9999999') return tid;
      }
      return (typeof PageType !== 'undefined' && PageType.getThreadId) ? PageType.getThreadId(true) || '' : '';
    } catch (e) { return ''; }
  }
  function markCurrentThreadQuoteRefs(root, contextThreadId) {
    if (!root || !contextThreadId) return;
    const tids = String(contextThreadId).trim();
    if (!tids) return;
    try {
      const els = Array.from(root.querySelectorAll('font[color="#789922"]'));
      els.forEach((el) => {
        if (el.dataset && el.dataset.xdexCurThreadMarkedTid === tids) return;
        const refNum = (String(el.textContent || '').match(/\d+/) || [])[0];
        if (!refNum) return;
        // 不再直接设 underline:统一由 applyQuoteAvailabilityStyle 按 thread 判定设置实线/虚线
        // xdexCurThreadMarkedTid 保持“已处理”防重复语义;xdexCurThreadRef 仅本串串首打,供样式判定
        if (el.dataset) {
          el.dataset.xdexCurThreadMarkedTid = tids;
          if (refNum === tids) el.dataset.xdexCurThreadRef = tids;
          else el.dataset.xdexCurThreadRef = '';
          // 已有 thread 判定时,打标记后立即刷新线型(防 mark 晚于 apply 时本串串首误显虚线)
          if (el.dataset.xdexQuoteAvail === 'thread') {
            try { applyQuoteAvailabilityStyle(el, 'thread'); } catch (e) {}
          }
        }
      });
      // 兑底:仅处理正文区(.h-threads-content)内未渲染成 <font> 的裸引用号(如 >>No.xxx),
      // 消息信息区(.h-threads-info / a.h-threads-info-id,如 "No.69299379" 编号链接)一律不改
      const contentRoots = [];
      try {
        if (root.matches && root.matches('.h-threads-content')) contentRoots.push(root);
        Array.prototype.push.apply(contentRoots, Array.from(root.querySelectorAll('.h-threads-content')));
      } catch (e) {}
      contentRoots.forEach((contentRoot) => {
        const walker = document.createTreeWalker(contentRoot, NodeFilter.SHOW_TEXT, {
          acceptNode(node) {
            if (!node.nodeValue || !/(>>\s*)?No\.\s*\d{4,}/.test(node.nodeValue)) return NodeFilter.FILTER_REJECT;
            const p = node.parentElement;
            if (p && p.tagName.toLowerCase() === 'font' && (p.getAttribute('color') || '').toLowerCase() === '#789922') return NodeFilter.FILTER_REJECT;
            if (p && p.closest && p.closest('a.h-threads-info-id, .h-threads-info')) return NodeFilter.FILTER_REJECT;
            return NodeFilter.FILTER_ACCEPT;
          }
        });
        const bareNodes = [];
        let bn;
        while ((bn = walker.nextNode())) bareNodes.push(bn);
        bareNodes.forEach((textNode) => {
          const text = textNode.nodeValue;
          const frag = document.createDocumentFragment();
          let cursor = 0;
          const re = /(>>\s*)?No\.\s*(\d{4,})/g;
          let m;
          while ((m = re.exec(text))) {
            const start = m.index, end = re.lastIndex;
            const refNum = m[2];
            if (start > cursor) frag.appendChild(document.createTextNode(text.slice(cursor, start)));
            const font = document.createElement('font');
            font.setAttribute('color', '#789922');
            font.textContent = text.slice(start, end);
            // 不再直接设 underline:统一由 applyQuoteAvailabilityStyle 按 thread 判定设置实线/虚线
            if (font.dataset) {
              font.dataset.xdexCurThreadMarkedTid = tids;
              if (refNum === tids) font.dataset.xdexCurThreadRef = tids;
              else font.dataset.xdexCurThreadRef = '';
              if (font.dataset.xdexQuoteAvail === 'thread') {
                try { applyQuoteAvailabilityStyle(font, 'thread'); } catch (e) {}
              }
            }
            frag.appendChild(font);
            cursor = end;
          }
          if (cursor < text.length) frag.appendChild(document.createTextNode(text.slice(cursor)));
          textNode.replaceWith(frag);
        });
      });
    } catch (e) {}
  }
  window.__xdexQuoteRefMark = {
    getContextThreadId: getRefContextThreadId,
    markCurrentThreadQuoteRefs: markCurrentThreadQuoteRefs
  };

  // —— 引用串可用性检测:仅保留当前页面会话结果,不写入 GM 存储 ——
  // 「已删/不存在」的标准响应不是 DOCTYPE 裸页,而是服务端照常渲染的 h-threads-item 空字段骨架:
  // data-threads-id 为空、No. 锚文本无编号、ID: 无饼干、正文为空。二者含义不同,不可混用。
  function isQuoteRefDeletedSkeleton(html) {
    if (typeof html !== 'string' || !html) return false;
    // 编号锚为空:活帖浮窗必然带 No.<数字>,此处只剩 “No.”
    const emptyNoAnchor = /<a\b[^>]*h-threads-info-id[^>]*>\s*No\.\s*<\/a>/i.test(html);
    // 容器存在但串号为空
    const emptyThreadsId = /data-threads-id\s*=\s*["']\s*["']/i.test(html);
    return emptyNoAnchor && emptyThreadsId;
  }
  function parseQuoteResponseForAvailability(html, tid) {
    if (html == null || String(html).trim() === '') return { kind: 'empty' };
    const text = String(html).trim();
    let parsed = null;
    try { parsed = JSON.parse(text); } catch (e) {}
    if (parsed && typeof parsed === 'object') {
      if (parsed.success === false || parsed.error) return { kind: 'reply' };
      if (parsed.id != null || parsed.content != null || Array.isArray(parsed.Replies)) return { kind: 'thread' };
    }
    if (/<!DOCTYPE html>\s*<html[^>]*>\s*<head[\s>]/i.test(text) && !/<(?:body|div|article|main)\b/i.test(text)) return { kind: 'empty' };
    if (tid && (text.includes('id="' + tid + '"') || text.includes("id='" + tid + "'") || text.includes('data-threads-id="' + tid + '"') || text.includes("data-threads-id='" + tid + "'"))) return { kind: 'thread' };
    // 【已停用·注释保留】类名 token 分支无法区分“主串存在”与“已删”:服务端对不存在的编号照样渲染
    // <div class="h-threads-item">,只是字段全空。实测删串响应即含该容器,无论宽分支还是收紧后的
    // token 分支都会误判为 thread(进而误插跳转按钮)。ref 原文只能用来判存在性,不能判主串。
    // if (/(?:class|id)=["'](?:[^"']*\s)?h-threads-item(?:-index)?(?:\s[^"']*)?["']/i.test(text)) return { kind: 'thread' };
    // 已删骨架优先于兜底的 reply 结论
    if (isQuoteRefDeletedSkeleton(text)) return { kind: 'empty' };
    return { kind: 'reply' };
  }

  function createQuoteAvailabilityQueue(options = {}) {
    const pending = new Set();
    const inFlight = new Set();
    const attempts = new Map();
    const MAX_ATTEMPTS = 3;
    const fetchFn = options.fetchFn || (() => Promise.reject(new Error('quote availability fetch unavailable')));
    const cacheGet = options.cacheGet || (() => null);
    const cacheSet = options.cacheSet || (() => {});
    const onResult = options.onResult || (() => {});
    let timer = null;
    let activeWorkers = 0;
    // 受限并发:一次刷新可能有数百个编号,全量并发会被 CDN 限流(限流错误即 unknown 的来源)
    // 并发数是实际的限流手段;GAP_MS 为单个 worker 相邻两次请求间的最小间隔,默认关闭,需要时由调用方显式传 gapMs
    // 判定必须用 Number.isFinite:Number(undefined) 是 NaN,而 `NaN != null` 恒为 true,会让默认分支变成死代码
    const CONCURRENCY = Math.max(1, Number(options.concurrency) || 2);
    const GAP_MS = Number.isFinite(Number(options.gapMs)) ? Math.max(0, Number(options.gapMs)) : 0;
    const delay = options.noDelay ? 0 : (Number(options.delay) || 120);
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const RETRY_BASE_MS = 1500;
    const retryLater = (tid) => {
      const n = (attempts.get(tid) || 0) + 1;
      attempts.set(tid, n);
      if (n >= MAX_ATTEMPTS) return false;
      setTimeout(() => {
        if (!cacheGet(tid) && !inFlight.has(tid) && !pending.has(tid)) {
          pending.add(tid);
          schedule();
        }
      }, RETRY_BASE_MS * n);
      return true;
    };
    const drainOne = async (tid) => {
      inFlight.add(tid);
      try {
        const cached = cacheGet(tid);
        if (cached && cached.kind) {
          onResult(tid, cached.kind);
          return;
        }
        const response = await fetchFn(tid);
        const parsed = parseQuoteResponseForAvailability(response && response.html, tid);
        const result = { kind: parsed.kind, t: Date.now() };
        // 只缓存明确判定(thread/reply/empty);失败不缓存,保留后续重试资格
        cacheSet(tid, result);
        attempts.delete(tid);
        onResult(tid, result.kind);
      } catch (e) {
        // 请求失败(限流/超时/断网):不入缓存,退避后自动重试,重试耗尽则本轮放弃
        const willRetry = retryLater(tid);
        onResult(tid, 'unknown');
        if (!willRetry) console.warn('[xdex-availability] 放弃重试', { id: tid });
      } finally {
        inFlight.delete(tid);
      }
    };
    // 常驻 worker:谁空了谁取下一个编号,消除“凑满一批 → 等最慢的一个 → 整批空等”的头部阻塞
    // 峰值在途请求数仍严格等于 CONCURRENCY,不增加任何 CDN 压力
    const startWorker = () => {
      activeWorkers++;
      const run = async () => {
        try {
          while (pending.size) {
            const id = pending.values().next().value;
            pending.delete(id);
            await drainOne(id);
            if (GAP_MS && pending.size) await sleep(GAP_MS);
          }
        } finally {
          activeWorkers--;
        }
        // 退出与入队存在竞态:仍有待办则补位,避免任务搁置到下一次 schedule
        if (pending.size && activeWorkers < CONCURRENCY) startWorker();
      };
      run();
    };
    const pump = () => {
      while (activeWorkers < CONCURRENCY && pending.size) startWorker();
    };
    const schedule = () => {
      if (timer !== null) return;
      if (options.noDelay) { pump(); return; }
      timer = setTimeout(() => { timer = null; pump(); }, delay);
    };
    return {
      // opts.priority:用户正在查看的编号提到队首,不排在数百个后台编号之后
      enqueue(tid, opts) {
        const id = String(tid || '').trim();
        if (!/^\d\d\d\d\d\d\d\d$/.test(id) || cacheGet(id) || inFlight.has(id)) return false;
        if ((attempts.get(id) || 0) >= MAX_ATTEMPTS) return false;
        const priority = !!(opts && opts.priority);
        if (pending.has(id)) {
          if (!priority) return false;
          pending.delete(id);
        }
        if (priority) {
          const rest = Array.from(pending);
          pending.clear();
          pending.add(id);
          rest.forEach((x) => pending.add(x));
        } else {
          pending.add(id);
        }
        schedule();
        return true;
      },
      // 已凭悬浮/点击的 ref 原文实时定论的编号:撤掉尚未发出的探测,省掉 api/ref+api/thread
      drop(tid) {
        const id = String(tid || '').trim();
        const had = pending.delete(id);
        if (had) attempts.delete(id);
        return had;
      },
      flushNow() {
        if (timer !== null) { clearTimeout(timer); timer = null; }
        pump();
        return new Promise((resolve) => {
          const wait = () => (pending.size || inFlight.size || activeWorkers) ? setTimeout(wait, 0) : resolve();
          wait();
        });
      },
      pendingSize() { return pending.size + inFlight.size; }
    };
  }

  function applyQuoteAvailabilityStyle(el, kind) {
    if (!el || !el.style) return;
    const value = kind === 'thread' || kind === 'empty' || kind === 'reply' || kind === 'unknown' ? kind : 'unknown';
    if (el.dataset) el.dataset.xdexQuoteAvail = value;
    // 统一不加粗:主串用横线区分;本串串首实线,其他主串虚线
    el.style.fontWeight = '';
    el.style.textDecoration = '';
    el.style.textDecorationThickness = '';
    if (value === 'thread') {
      const isCurrentThread = !!(el.dataset && el.dataset.xdexCurThreadRef);
      el.style.textDecoration = isCurrentThread ? 'underline' : 'underline dashed';
      el.style.textDecorationThickness = '2px';
    }
    el.style.opacity = value === 'empty' ? '0.5' : '';
  }

  // 提取元素文本中的引用编号:仅标准(>>No. / No.)与拓展(独立 8 位数字 / >>8位)格式;
  // 不含编号的绿色文本(如 ">>如我所见")返回 null,视为非引用,不判定、不改样式
  function getQuoteRefIdFromText(text) {
    const m = String(text || '').match(/(?:>>)?No\.\s?(\d{8})\b|(?:>>)?(?<!\d)(\d{8})(?!\d)/);
    return m ? (m[1] || m[2]) : null;
  }

  function probeQuoteAvailability(el, context = {}) {
    const text = String(el && el.textContent || '');
    // 仅当文本是标准/拓展引用时才判定;不含引用编号的绿色 font(如 ">>如我所见")不是引用,直接跳过
    const tid = getQuoteRefIdFromText(text);
    if (!tid) return { kind: 'skip', queued: false };
    const state = context.state || {};
    if (state.extendQuoteAvailabilityDetection === false || state.extendQuote === false) return { kind: 'unknown', queued: false };
    const cached = context.cache && context.cache[tid];
    const kind = cached && cached.kind ? cached.kind : 'unknown';
    applyQuoteAvailabilityStyle(el, kind);
    if (cached || !context.queueSet) return { kind, queued: false };
    context.queueSet(tid);
    return { kind, queued: true };
  }

  const quoteAvailabilityCache = Object.create(null);
  let quoteAvailabilityQueue = null;
  // 跳转按钮补插监听:key = 引用串号,value = Set<回调>(浮窗打开后注册,判定为 thread 时触发)
  const quoteJumpListeners = new Map();
  function onQuoteAvailThread(tid, fn) {
    const id = String(tid || '');
    if (!id || typeof fn !== 'function') return;
    if (!quoteJumpListeners.has(id)) quoteJumpListeners.set(id, new Set());
    quoteJumpListeners.get(id).add(fn);
  }
  function offQuoteAvailThread(tid, fn) {
    const id = String(tid || '');
    const set = quoteJumpListeners.get(id);
    if (set) { set.delete(fn); if (!set.size) quoteJumpListeners.delete(id); }
  }
  let xdexAvailSummary = { thread: 0, reply: 0, empty: 0, unknown: 0 };
  let xdexAvailSummaryTimer = null;
  function xdexAvailLogSummary() {
    const s = xdexAvailSummary;
    const total = s.thread + s.reply + s.empty + s.unknown;
    if (total <= 0) return;
    console.log('[xdex-availability] 判定汇总', { thread: s.thread, reply: s.reply, empty: s.empty, unknown: s.unknown });
    xdexAvailSummary = { thread: 0, reply: 0, empty: 0, unknown: 0 };
  }
  function getQuoteAvailabilityState() {
    try {
      return Object.assign({}, SettingPanel.defaults, SettingPanel.state || {}, GM_getValue(SettingPanel.key, {}));
    } catch (e) { return { extendQuote: true, extendQuoteAvailabilityDetection: true }; }
  }
  function fetchQuoteAvailability(tid) {
    const id = encodeURIComponent(tid);
    const refUrl = `https://api.nmb.best/api/ref?id=${id}`;
    const threadUrl = `https://api.nmb.best/api/thread?id=${id}&page=1`;
    const headers = typeof getPostHistoryApiCookieHeaders === 'function'
      ? getPostHistoryApiCookieHeaders()
      : null;
    const readResponse = (resp) => {
      const raw = resp && (resp.response || resp.responseText || '');
      if (raw && typeof raw === 'object') return raw;
      try { return JSON.parse(String(raw || '')); } catch (e) { return null; }
    };
    return gmRequest(refUrl, 'text', headers).then((refResp) => {
      const ref = readResponse(refResp);
      // ref 存在且非业务失败 → 目标编号确实存在;是否主串由 thread 判定
      if (!ref || ref.success === false || ref.error) return { html: '', httpStatus: 200 };
      return gmRequest(threadUrl, 'text', headers).then((threadResp) => {
        const thread = readResponse(threadResp);
        if (thread && thread.success === false) return { html: JSON.stringify({ success: false, error: thread.error || 'reply' }), httpStatus: 200 };
        return { html: JSON.stringify(thread || {}), httpStatus: 200 };
      }).catch((err) => {
        // thread 明确 404 → 该编号不是主串,是回复
        if (err && /HTTP 404\b/.test(String(err.message || err))) {
          return { html: JSON.stringify({ success: false, error: 'reply' }), httpStatus: 404 };
        }
        throw err;
      });
    });
  }
  // 判定结果统一出口:样式回填、跳转按钮补插、汇总日志。队列回调与悬浮/点击实时回填共用
  function applyQuoteAvailabilityResult(tid, kind) {
    // 检测关闭时不得改样式:多标签页远程关闭后,已入队探测的残余回调不得再回填全文档(L2 缺口)
    if (!isQuoteAvailabilityEnabled()) return;
    // 仅对真正包含该引用编号的元素应用样式;不含编号的绿色文本不改动
    document.querySelectorAll('font[color="#789922"]').forEach((el) => {
      if (getQuoteRefIdFromText(el.textContent) === tid) applyQuoteAvailabilityStyle(el, kind);
    });
    // 判定为 thread(主串) 时,通知所有打开该引用浮窗的跳转按钮补插
    if (kind === 'thread' && quoteJumpListeners && quoteJumpListeners.size) {
      const set = quoteJumpListeners.get(String(tid));
      if (set && set.size) {
        [...set].forEach((fn) => { try { fn(String(tid)); } catch (e) {} });
      }
    }
    // 批量汇总:一次扫掠只打一条汇总日志,不逐 id 输出(发送消息后整片新引用号会一次性判定,逐行会刷屏)
    if (xdexAvailSummary[kind] != null) xdexAvailSummary[kind] += 1;
    if (xdexAvailSummaryTimer) return;
    xdexAvailSummaryTimer = setTimeout(() => {
      xdexAvailSummaryTimer = null;
      xdexAvailLogSummary();
    }, 300);
  }
  function getQuoteAvailabilityQueue() {
    if (quoteAvailabilityQueue) return quoteAvailabilityQueue;
    quoteAvailabilityQueue = createQuoteAvailabilityQueue({
      fetchFn: fetchQuoteAvailability,
      cacheGet: (tid) => quoteAvailabilityCache[tid],
      cacheSet: (tid, entry) => { quoteAvailabilityCache[tid] = entry; },
      onResult: applyQuoteAvailabilityResult
    });
    return quoteAvailabilityQueue;
  }
  // 悬浮/点击路径自己会打 /Home/Forum/ref,命中“已删骨架”即得到实时可用性结论,
  // 据此直接定论并撤掉尚未发出的探测,省掉后台的 api/ref + api/thread 两次重复请求
  // 【已停用·注释保留】DOCTYPE 裸页对应“请求没返回可渲染内容”(登录跳转/错误页/被拦),与“已删”是两件事,
  // 不作为删串判据;悬浮路径原有的“不渲染浮窗”早退仍按该标记执行。
  const QUOTE_REF_EMPTY_MARK = '<!DOCTYPE html><html><head>';
  // 悬浮/点击回填与优先探测同样受开关约束:检测关闭时不得建队、不得改样式
  function isQuoteAvailabilityEnabled() {
    const state = getQuoteAvailabilityState();
    return state.extendQuoteAvailabilityDetection !== false && state.extendQuote !== false;
  }
  function settleQuoteRefAsEmpty(tid, html) {
    const id = String(tid || '').trim();
    if (!/^\d{8}$/.test(id)) return false;
    // 【已停用·注释保留】旧判据把 DOCTYPE 裸页当“已删”,实际打不到标准删串响应(它是空字段骨架,无 DOCTYPE)
    // if (typeof html !== 'string' || html.indexOf(QUOTE_REF_EMPTY_MARK) < 0) return false;
    if (!isQuoteRefDeletedSkeleton(html)) return false;
    if (!isQuoteAvailabilityEnabled()) return false;
    if (!quoteAvailabilityCache[id] || !quoteAvailabilityCache[id].kind) {
      quoteAvailabilityCache[id] = { kind: 'empty', t: Date.now() };
    }
    if (quoteAvailabilityQueue) quoteAvailabilityQueue.drop(id);
    applyQuoteAvailabilityResult(id, 'empty');
    return true;
  }
  // 用户正在查看的编号提到队首,不必排在数百个后台编号之后
  function prioritizeQuoteProbe(tid) {
    const id = String(tid || '').trim();
    if (!/^\d{8}$/.test(id)) return;
    if (!isQuoteAvailabilityEnabled()) return;
    if (quoteAvailabilityCache[id] && quoteAvailabilityCache[id].kind) return;
    getQuoteAvailabilityQueue().enqueue(id, { priority: true });
  }
  // 视口门控:只对进入视口(含预读边距)的引用号入队,使探测进度跟随阅读位置,
  // 而不是页面一打开就按 DOM 顺序排队数百个编号、用户读到哪都还没轮到哪
  const QUOTE_PROBE_ROOT_MARGIN = '200px';
  let quoteProbeObserver = null;
  function observeQuoteProbe(el, tid) {
    const id = String(tid || '').trim();
    if (!/^\d{8}$/.test(id)) return;
    if (typeof IntersectionObserver !== 'function') { getQuoteAvailabilityQueue().enqueue(id); return; }
    if (!quoteProbeObserver) {
      quoteProbeObserver = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;
          const waiting = entry.target.__xdexQuoteProbeTid;
          delete entry.target.__xdexQuoteProbeTid;
          quoteProbeObserver.unobserve(entry.target);
          if (waiting && isQuoteAvailabilityEnabled()) getQuoteAvailabilityQueue().enqueue(waiting);
        });
      }, { rootMargin: QUOTE_PROBE_ROOT_MARGIN });
    }
    if (el.__xdexQuoteProbeTid === id) return; // 已在观察同一编号,避免重复 observe
    el.__xdexQuoteProbeTid = id;
    quoteProbeObserver.observe(el);
  }
  function refreshQuoteAvailability(root = document) {
    const state = getQuoteAvailabilityState();
    if (state.extendQuoteAvailabilityDetection === false || state.extendQuote === false) return;
    getQuoteAvailabilityQueue();
    const fonts = [];
    if (root && root.matches && root.matches('font[color="#789922"]')) fonts.push(root);
    if (root && root.querySelectorAll) fonts.push(...root.querySelectorAll('font[color="#789922"]'));
    fonts.forEach((el) => probeQuoteAvailability(el, {
      state,
      cache: quoteAvailabilityCache,
      queueSet: (tid) => observeQuoteProbe(el, tid)
    }));
  }
  window.__xdexQuoteAvailability = { refresh: refreshQuoteAvailability, probe: probeQuoteAvailability };

  //引用格式拓展
  function extendQuote(root = document) {
    return startupPerfDebug.measure('extendQuote', () => {
    const ROOT_SELECTOR = '.h-threads-content, .h-post-form-input, .xdex-post-history-thread';
    const QUOTE_COLOR = '#789922';
    // 在容器内遍历纯文本节点,避免破坏现有标签
    const quoteRoots = root.matches?.(ROOT_SELECTOR)
      ? [root]
      : Array.from(root.querySelectorAll(ROOT_SELECTOR));
    quoteRoots.forEach(root => {
        const walker = document.createTreeWalker(
            root,
            NodeFilter.SHOW_TEXT,
            {
                acceptNode(node) {
                    // 没数字直接跳过
                    if (!node.nodeValue || !/\d/.test(node.nodeValue)) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    // 已经在 quote 的 <font color="#789922"> 内,跳过
                    const p = node.parentElement;
                    if (p && p.tagName.toLowerCase() === 'font' && (p.getAttribute('color') || '').toLowerCase() === QUOTE_COLOR) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    return NodeFilter.FILTER_ACCEPT;
                }
            }
        );
        const textNodes = [];
        let n;
        while ((n = walker.nextNode())) textNodes.push(n);
        textNodes.forEach(processTextNode);
        // 原生标准引用号(>>No.12345678 已由原站渲染为 <font color="#789922">)会被 walker 跳过,
        // 这里统一补标:引用号 == 当前串号 → 同色下划线
        try { markCurrentThreadQuoteRefs(root, getRefContextThreadId(root)); } catch (e) {}
      try { refreshQuoteAvailability(root); } catch (e) {}
    });
    function processTextNode(textNode) {
        const text = textNode.nodeValue;
        // 两类模式:No.12345678 与 单独 8 位数字
        const patterns = [
          { name: 'no',  regex: /(?:>>)?No\.(\d{8})\b/g },      // 匹配 No.12345678 或 >>No.12345678
          { name: 'num', regex: /(?:>>)?(?<!\d)(\d{8})(?!\d)/g } // 匹配 12345678 或 >>12345678
        ];
        const frag = document.createDocumentFragment();
        let cursor = 0;
        let changed = false;
        while (true) {
            const next = findNextMatch(text, cursor, patterns);
            if (!next) break;
            const { start, end } = next;
            // 若匹配前紧挨着 ">>",说明是标准引用的一部分,跳过这次,继续后移
            // if (start >= 2 && text[start - 2] === '>' && text[start - 1] === '>') {
            //     // 只推进一位,继续找后面的匹配,避免卡住
            //     cursor = start + 1;
            //     continue;
            // }
            // 追加匹配前的原始文本
            if (start > cursor) {
                frag.appendChild(document.createTextNode(text.slice(cursor, start)));
            }
            // 用与标准引用一致的 <font color="#789922"> 包裹,但不添加 ">>"
            const font = document.createElement('font');
            font.setAttribute('color', QUOTE_COLOR);
            // 直接保留原始匹配文本(可能是 "No.12345678" 或 "12345678")
            font.textContent = text.slice(start, end);
            // 标识引用号是否为当前串串号:是则在引用号下方添加同色横线(underline 贴近文字)
            // 回应模式行(实际容器 .h-post-form-grid / .js-reply-mode-row)内的 No.xxxx 不添加横线
            const refNum = (font.textContent.match(/\d+/) || [])[0];
            const modeTextEl = textNode.parentElement && textNode.parentElement.closest ? textNode.parentElement.closest('.h-post-form-grid, .js-reply-mode-text, .js-reply-mode-row') : null;
            if (refNum && !modeTextEl && refNum === getRefContextThreadId(textNode.parentElement)) {
              // 不再直接设 underline:统一由 applyQuoteAvailabilityStyle 按 thread 判定设置实线/虚线
              if (font.dataset) {
                font.dataset.xdexCurThreadMarkedTid = refNum;
                font.dataset.xdexCurThreadRef = refNum;
                if (font.dataset.xdexQuoteAvail === 'thread') {
                  try { applyQuoteAvailabilityStyle(font, 'thread'); } catch (e) {}
                }
              }
            }
            frag.appendChild(font);
            cursor = end;
            changed = true;
        }
        if (!changed) return;
        // 末尾剩余文本
        if (cursor < text.length) {
            frag.appendChild(document.createTextNode(text.slice(cursor)));
        }
        textNode.replaceWith(frag);
    }
    // 在多正则间找到下一处最早的匹配
    function findNextMatch(text, fromIndex, patterns) {
        let best = null;
        for (const p of patterns) {
            p.regex.lastIndex = fromIndex;
            const m = p.regex.exec(text);
            if (m) {
                const start = m.index;
                const end = start + m[0].length;
                if (!best || start < best.start) {
                    best = { start, end };
                }
            }
        }
        return best;
    }
    try { refreshQuoteAvailability(root); } catch (e) {}
    }, () => startupPerfDebug.summarizeRoot(root));
  }
  function initExtendedContent(root) {
    return startupPerfDebug.measure('initExtendedContent', () => {
    const $root = $(root || document);
    ensureRefViewLayoutStyle();
    renderHiddenTextContent(root);  // 先处理隐藏文本,再挂处理器,避免 $content.html() 重写破坏处理器
    try { injectSubscriptionExButton(root || document); } catch (e) {}
    // —— 在捕获阶段接管原生引用浮窗,避免原站先显示未处理内容 ——
    const _xvFindResult = $root.find("font[color='#789922']");
    // console.log('[xv-ref-find] $root.find result:', _xvFindResult.length, 'root:', $root[0]?.tagName, $root[0]?.className);
    _xvFindResult.add($root.filter("font[color='#789922']"))
      .filter(function () {
        return /(No\.\d{8}|\d{8})/.test($(this).text());
      })
      .off('mouseenter.ext')
      .each(function () {
        const quoteEl = this;
        if (quoteEl.__xdexRefHoverHandler) {
          quoteEl.removeEventListener('mouseover', quoteEl.__xdexRefHoverHandler, true);
          quoteEl.removeEventListener('mouseenter', quoteEl.__xdexRefHoverHandler, true);
        }
        // console.log('[xv-ref-attach] attaching handler to:', quoteEl.textContent, 'parent:', quoteEl.parentElement.tagName);
        quoteEl.__xdexRefHoverHandler = function (event) {
          if (event.type === 'mouseover' && event.relatedTarget && quoteEl.contains(event.relatedTarget)) return;
          event.preventDefault();
          event.stopPropagation();
          event.stopImmediatePropagation();
          const match = /\d+/.exec($(quoteEl).text());
          if (!match) return;
          const tid = match[0];
          const seq = (window.__xdexRefViewRequestSeq || 0) + 1;
          window.__xdexRefViewRequestSeq = seq;
          const $rv = $("#h-ref-view").off().stop(true, true).hide().css('visibility', 'hidden');
          // 换引用前先清掉上一次有图布局残留的 width/class,避免空/无图内容继承超长宽度
          if (typeof resetRefViewLayoutState === 'function') resetRefViewLayoutState($rv[0]);
          // console.log('[xv-ref] handler fired, tid=' + tid + ', quoteText=' + $(quoteEl).text());
          // console.log('[xv-ref] #h-ref-view exists:', $('#h-ref-view').length, 'display:', $('#h-ref-view').css('display'));
          $.get('/Home/Forum/ref?id=' + tid)
            .done(function (data) {
              if (seq !== window.__xdexRefViewRequestSeq) return;
              // 空壳即实时可用性结论:直接定论并撤掉后台探测;否则把该编号提到队首优先判定
              if (!settleQuoteRefAsEmpty(tid, data)) prioritizeQuoteProbe(tid);
              if (data.indexOf('<!DOCTYPE html><html><head>') >= 0) return;
              $rv.html(data).css({
                top: $(quoteEl).offset().top,
                left: $(quoteEl).offset().left
              });
              // html 替换后再次复位,防止 jQuery/原站样式把旧 inline width 带回来
              if (typeof resetRefViewLayoutState === 'function') resetRefViewLayoutState($rv[0]);
              try {
                const refEl = $rv[0];
                hideEmptyTitleAndEmail(refEl);
                renderHiddenTextContent(refEl);
                if (typeof extendQuote === 'function') extendQuote(refEl);
                if (typeof initExtendedContent === 'function') initExtendedContent(refEl);
                try { refreshQuoteAvailability(refEl); } catch (e) {}
                const _cfg = typeof getFilterConfig === 'function' ? getFilterConfig() : Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
                markAllCookies(_cfg.markedGroups || [], refEl);
                if (_cfg.enableImageHideMode) applyImageHideMode(_cfg.applyImageHideMode || 'default', refEl);
                if (_cfg.enableAutoUrlLinkify) runAutoUrlLinkify(refEl);
                updateRefViewImageLayout(refEl, quoteEl);
              } catch (e) {}
              // console.log('[xv-ref] AJAX done, showing popup, offset:', $(quoteEl).offset());
              $rv.css('visibility', '').show();
              // console.log('[xv-ref] popup shown, display:', $rv.css('display'), 'visibility:', $rv.css('visibility'));
            });
        };
        quoteEl.addEventListener('mouseover', quoteEl.__xdexRefHoverHandler, true);
        quoteEl.addEventListener('mouseenter', quoteEl.__xdexRefHoverHandler, true);
      });
    // —— 新增:处理 [h]...[/h] 隐藏文本 ——
    try { refreshQuoteAvailability(root || document); } catch (e) {}
    }, () => startupPerfDebug.summarizeRoot(root || document));
  }

  /* --------------------------------------------------
   * tag 10. 创建拓展坞+reply按钮呼出回复悬浮窗
   * -------------------------------------------------- */
  function isBoardThreadListPage() {
    // 版块页/时间线页:顶层串列表(串内页无串列表,保持三按钮)
    return PageType.isBoardPage() || PageType.isTimelineAnyPage();
  }
  function scrollPageToY(targetY, behavior) {
    try {
      window.scrollTo({ top: targetY, left: 0, behavior: behavior || 'smooth' });
    } catch (e) {
      try { window.scrollTo(0, targetY); } catch (e2) {
        document.documentElement.scrollTop = targetY;
        if (document.body) document.body.scrollTop = targetY;
      }
    }
  }
  function isThreadItemVisible(el) {
    if (!el || !el.isConnected) return false;
    try {
      const cs = window.getComputedStyle(el);
      if (cs.display === 'none' || cs.visibility === 'hidden') return false;
      const r = el.getBoundingClientRect();
      return r.width > 0 && r.height > 0;
    } catch (e) { return true; }
  }
  function getBoardThreadItems() {
    // 只收集可见顶层串:过滤 display:none 的屏蔽/隐藏项,避免其 rect.top=0 干扰基准判定
    return Array.from(document.querySelectorAll('.h-threads-list > .h-threads-item[data-threads-id]'))
      .filter(isThreadItemVisible);
  }
  function isSeamlessPagingEnabled() {
    try {
      if (SettingPanel && SettingPanel.state) return !!SettingPanel.state.enableSeamlessPaging;
      const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      return !!cfg.enableSeamlessPaging;
    } catch (e) { return false; }
  }
  function triggerNextPageLoad() {
    logRightSidebarDocker('thread-nav-nextpage', { seamlessEnabled: isSeamlessPagingEnabled() });
    // 无缝翻页启用:与触底/手动翻页一致,调用 loadNext(自动/手动模式共用)
    if (isSeamlessPagingEnabled() && window.SeamlessPaging && typeof window.SeamlessPaging.loadNext === 'function') {
      try {
        window.SeamlessPaging.loadNext();
        return true;
      } catch (e) {
        console.warn('[rightSidebarDocker] loadNext 调用失败', e);
      }
    }
    // 非无缝:跳转站点分页「下一页」链接
    try {
      const pager = document.querySelector('ul.uk-pagination.uk-pagination-left.h-pagination');
      const nextLi = pager && Array.from(pager.querySelectorAll('li')).find((li) => {
        const t = (li.textContent || '').trim();
        return /下一页|下页|Next|›|»|→/i.test(t) && !li.classList.contains('uk-disabled');
      });
      const link = nextLi && nextLi.querySelector('a[href]');
      if (link && link.href) {
        let url = null;
        try { url = new URL(link.href, location.href); } catch (e) {}
        // 仅当解析出的地址与当前页不同才跳转,避免异常 href(如首页/空链)造成误跳
        if (url && url.href !== location.href) {
          logRightSidebarDocker('thread-nav-nextpage-native', { href: url.href });
          location.href = url.href;
          return true;
        }
      }
    } catch (e) {}
    return false;
  }
  function getWebdavPageScrollY() {
    return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
  }
  let navLastAlign = null; // {tid, y, ts}:最近一次“对齐当前串顶”的目标,用于抗布局漂移判定
  function scrollToAdjacentThread(direction) {
    const items = getBoardThreadItems();
    if (!items.length) {
      logRightSidebarDocker('thread-nav-skip', { reason: 'no-thread-items' });
      if (typeof toast === 'function') toast('当前页面没有串');
      return;
    }
    // 即时判定(每次点击实时计算,无缓存、不依赖上次点击状态):
    // 当前串 = 视口顶部线正压着的串(top<=0 && bottom>0 的第一个);
    // 页面顶部未滚动 → 第一个 top>=0 的串;全部滚出视口上方 → 最后一个
    let base = -1;
    for (let i = 0; i < items.length; i++) {
      const r = items[i].getBoundingClientRect();
      if (r.top <= 0 && r.bottom > 0) { base = i; break; }
      if (r.top >= 0) { base = i; break; }
    }
    if (base < 0) base = items.length - 1;
    const curTop = items[base].getBoundingClientRect().top;
    // 上一串:当前串顶部已被滚出视口上方(顶部线压在其中间)→ 先滚回当前串顶部
    // 抗漂移判定:若刚对齐过同一个串且页面未被手动滚动(无缝翻页/懒加载造成的布局漂移会让
    // curTop 持续为负,此时按像素阈值判断会永远“未对齐”),直接视为已对齐并跳前串
    if (direction < 0 && curTop < 0) {
      const now = Date.now();
      const scrollY = getWebdavPageScrollY();
      const tid = items[base].getAttribute('data-threads-id');
      if (navLastAlign && navLastAlign.tid === tid
          && now - navLastAlign.ts < 10000
          && Math.abs(scrollY - navLastAlign.y) < 3) {
        navLastAlign = null; // 已对齐(即使布局漂移后 curTop 仍为负):走下方跳转分支
      } else {
        const y = curTop + scrollY;
        logRightSidebarDocker('thread-nav-align-current', { base, tid, y });
        navLastAlign = { tid, y, ts: now };
        scrollPageToY(y, 'auto');
        return;
      }
    }
    navLastAlign = null;
    const target = base + direction;
    if (target < 0 || target >= items.length) {
      logRightSidebarDocker('thread-nav-boundary', { direction, base, target, total: items.length });
      // 向下到末尾:主动加载下一页(无缝/手动按设置,类似触底翻页);向上则提示
      if (direction > 0 && triggerNextPageLoad()) {
        return;
      }
      if (typeof toast === 'function') toast(direction > 0 ? '已经是最后一个串' : '已经是第一个串');
      return;
    }
    const el = items[target];
    const y = el.getBoundingClientRect().top + getWebdavPageScrollY();
    logRightSidebarDocker('thread-nav', { direction, base, target, tid: el.getAttribute('data-threads-id'), y, scrollTop: getWebdavPageScrollY() });
    scrollPageToY(y);
  }
  function buildRightSidebarDockerHtml() {
    const withThreadNav = isBoardThreadListPage();
    return `
        <div class="hld__docker">
            <div class="hld__docker-sidebar">
                <svg viewBox="0 0 1024 1024" width="64" height="64">
                    <path d="M518.3 824.05c-7.88 0-15.76-2.97-21.69-9L215.25 533.65c-5.73-5.73-9-13.61-9-21.69s3.27-15.96 9-21.69l281.4-281.4c11.97-11.97 31.41-11.97 43.39 0s11.97 31.41 0 43.39L280.33 511.95l259.71 259.71c11.97 11.97 11.97 31.41 0 43.39-5.94 6.04-13.72 9-21.69 9z" fill="#888"/>
                    <path d="M787.16 772.89c-7.88 0-15.76-2.97-21.69-9L535.23 533.65c-11.97-11.97-11.97-31.41 0-43.39l230.24-230.24c11.97-11.97 31.41-11.97 43.39 0s11.97 31.41 0 43.39L600.31 511.95l208.55 208.55c11.97 11.97 11.97 31.41 0 43.39-5.94 6.04-13.72 9-21.69 9z" fill="#888"/>
                </svg>
            </div>
            <div class="hld__docker-btns">
                <div data-type="TOP" title="回到顶部"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 8 L18 15 H6 Z"/><path d="M12 3 L16 7 H8 Z"/></svg></div>
                ${withThreadNav ? '<div data-type="PREV-THREAD" title="上一个串"><svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 6 L19 17 H5 Z"/></svg></div>' : ''}
                <div data-type="REPLY"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path style="fill:none" d="M19.9 4.8V14.2a1.5 1.5 0 0 1-1.5 1.5H4.1"/><path style="fill:none" d="M8.8 19.2L4.1 15.7l4.7-3.5"/></svg></div>
                ${withThreadNav ? '<div data-type="NEXT-THREAD" title="下一个串"><svg viewBox="0 0 24 24" width="16" height="16"><path d="M12 18 L5 7 H19 Z"/></svg></div>' : ''}
                <div data-type="BOTTOM" title="到底部"><svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 16 L6 9 H18 Z"/><path d="M12 21 L8 17 H16 Z"/></svg></div>
            </div>
        </div>
    `;
  }
  function ensureRightSidebarShellStyle() {
    // 可重复刷新文本:热更新后避免旧 transition:all 残留
    let style = document.getElementById('xdex-right-sidebar-shell-style');
    if (!style) {
      style = document.createElement('style');
      style.id = 'xdex-right-sidebar-shell-style';
      (document.head || document.documentElement).appendChild(style);
    }
    style.textContent = `
      #h-tool { display: none !important; }
      /* 只过渡几何属性,缩短时长,减轻首屏卡顿体感 */
      .hld__docker {
        position: fixed; height: 80px; width: 30px; bottom: 180px; right: 0;
        transition: width .12s ease, height .12s ease, bottom .12s ease;
        z-index: 9998;
      }
      .hld__docker:hover,
      .hld__docker.is-hover { width: 150px; height: 300px; bottom: 75px; }
      .hld__docker:has(.hld__docker-sidebar:hover) { width: 150px; height: 300px; bottom: 75px; }
      /* 进页约 1.5s 内关闭尺寸过渡,悬停几乎即时展开,躲开 ready/batch 长任务掉帧 */
      .hld__docker.xdex-docker-boot,
      .hld__docker.xdex-docker-boot:hover,
      .hld__docker.xdex-docker-boot.is-hover,
      .hld__docker.xdex-docker-boot:has(.hld__docker-sidebar:hover) {
        transition: none;
      }
      .hld__docker-sidebar { background: #fff; position: fixed; height: 50px; width: 20px; bottom: 195px; right: 0; display: flex; justify-content: center; align-items: center; border: 1px solid #CCC; box-shadow: 0 0 1px #333; border-right: none; border-radius: 5px 0 0 5px; }
      .hld__docker-btns { position: absolute; top: 0; left: 50px; bottom: 0; right: 50px; display: flex; justify-content: center; align-items: center; flex-direction: column; }
      /* 三钮更快显隐,与坞尺寸解耦 */
      .hld__docker .hld__docker-btns>div {
        opacity: 0; flex-shrink: 0; pointer-events: none;
        transition: opacity .06s ease;
      }
      .hld__docker:hover .hld__docker-btns>div,
      .hld__docker.is-hover .hld__docker-btns>div {
        opacity: 1; pointer-events: auto;
      }
      .hld__docker:has(.hld__docker-sidebar:hover) .hld__docker-btns>div {
        opacity: 1; pointer-events: auto;
      }
      .hld__docker.xdex-docker-boot .hld__docker-btns>div { transition: none; }
      /* 上一串/下一串/TOP/BOTTOM:面积缩小为原本的 2/3(直径 50px → 约 41px);REPLY 保持 50px;间隙 10px → 7px(缩 1/3) */
      .hld__docker-btns>div { background: #fff; border: 1px solid #CCC; box-shadow: 0 0 1px #444; width: 50px; height: 50px; border-radius: 50%; margin: 7px 0; cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 20px; font-weight: bold; color: #333; transition: background .12s ease, transform .12s ease, opacity .06s ease; }
      .hld__docker-btns>div:hover { background: #f0f0f0; transform: scale(1.1); }
      .hld__docker-btns>div svg { display: block; }
      .hld__docker-btns>div svg path { fill: currentColor; }
      .hld__docker-btns>div[data-type="PREV-THREAD"],
      .hld__docker-btns>div[data-type="NEXT-THREAD"],
      .hld__docker-btns>div[data-type="TOP"],
      .hld__docker-btns>div[data-type="BOTTOM"] { width: 41px; height: 41px; font-size: 16px; }
      /* 固定模式:dock 收窄贴右、三按钮常显、隐藏左侧把手(不依赖 hover/:has,回退机制不受影响)
         提高特异性压过 qp-style 后注入的 :hover/is-hover/:has 展开规则,固定下悬浮不抖动 */
      .hld__docker.xdex-dock-fixed { width: 60px; height: 300px; bottom: 75px; }
      .hld__docker.xdex-dock-fixed:hover,
      .hld__docker.xdex-dock-fixed.is-hover,
      .hld__docker.xdex-dock-fixed:has(.hld__docker-sidebar:hover) { width: 60px; height: 300px; bottom: 75px; }
      .hld__docker.xdex-dock-fixed .hld__docker-sidebar { display: none; }
      .hld__docker.xdex-dock-fixed .hld__docker-btns { left: 0; right: 0; }
      .hld__docker.xdex-dock-fixed .hld__docker-btns>div { opacity: 1; pointer-events: auto; }
      .hld__docker.xdex-dock-boot.xdex-dock-fixed { transition: none; }
    `;
  }
  function logRightSidebarDocker(stage, detail) {
    try {
      console.log('[rightSidebarDocker]', stage, detail || {});
    } catch (e) {}
  }
  function getRightSidebarReplyForm() {
    return document.querySelector('form[action="/Home/Forum/doReplyThread.html"]')
      || document.querySelector('#h-post-form form[action="/Home/Forum/doPostThread.html"]');
  }
  // 旧核可能不认 scrollTo({ top, behavior });先对象写法,失败再回退坐标写法。
  function scrollPageByDockerButton(kind) {
    const beforeY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
    const maxY = Math.max(
      document.documentElement.scrollHeight || 0,
      document.body.scrollHeight || 0
    );
    const targetY = kind === 'TOP' ? 0 : maxY;
    let method = 'none';
    let errorMessage = '';
    try {
      if (typeof window.scrollTo === 'function') {
        try {
          window.scrollTo({ top: targetY, left: 0, behavior: 'smooth' });
          method = 'scrollTo-options-smooth';
        } catch (optionsErr) {
          window.scrollTo(0, targetY);
          method = 'scrollTo-coords-after-options-throw';
          errorMessage = optionsErr && optionsErr.message ? optionsErr.message : String(optionsErr);
        }
      } else if (typeof window.scroll === 'function') {
        window.scroll(0, targetY);
        method = 'scroll-coords';
      } else {
        document.documentElement.scrollTop = targetY;
        if (document.body) document.body.scrollTop = targetY;
        method = 'scrollTop-assign';
      }
    } catch (err) {
      errorMessage = err && err.message ? err.message : String(err);
      try {
        document.documentElement.scrollTop = targetY;
        if (document.body) document.body.scrollTop = targetY;
        method = method === 'none' ? 'scrollTop-assign-fallback' : (method + '+scrollTop-fallback');
      } catch (err2) {
        errorMessage = (errorMessage ? errorMessage + ' | ' : '') + (err2 && err2.message ? err2.message : String(err2));
        method = 'failed';
      }
    }
    // 下一帧核对是否真的动了;旧核常“吃掉 options 且不抛错”,需再回退坐标写法
    setTimeout(() => {
      let afterY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
      let moved = Math.abs(afterY - beforeY) > 1 || (kind === 'TOP' && afterY <= 1);
      // TOP 时 before 已接近 0 也算到位;BOTTOM 需明显靠近 target
      if (kind === 'BOTTOM') {
        moved = Math.abs(afterY - beforeY) > 1 || afterY >= Math.max(0, targetY - 50);
      }
      if (!moved && method.indexOf('scrollTo-options') === 0) {
        try {
          window.scrollTo(0, targetY);
          method = 'scrollTo-coords-after-options-noop';
          afterY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
          moved = Math.abs(afterY - beforeY) > 1 || (kind === 'TOP' ? afterY <= 1 : afterY >= Math.max(0, targetY - 50));
        } catch (coordErr) {
          errorMessage = (errorMessage ? errorMessage + ' | ' : '') + (coordErr && coordErr.message ? coordErr.message : String(coordErr));
          try {
            document.documentElement.scrollTop = targetY;
            if (document.body) document.body.scrollTop = targetY;
            method = 'scrollTop-assign-after-options-noop';
            afterY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
            moved = Math.abs(afterY - beforeY) > 1 || (kind === 'TOP' ? afterY <= 1 : afterY >= Math.max(0, targetY - 50));
          } catch (err3) {
            method = 'failed';
            errorMessage = (errorMessage ? errorMessage + ' | ' : '') + (err3 && err3.message ? err3.message : String(err3));
          }
        }
      }
      logRightSidebarDocker('scroll-result', {
        kind,
        method,
        beforeY,
        targetY,
        afterY,
        moved,
        errorMessage: errorMessage || undefined,
        supportsScrollTo: typeof window.scrollTo === 'function',
      });
      // BOTTOM:图片/无缝内容可能使页面在滚动中继续增高,延迟后再校正一次
      if (kind === 'BOTTOM') {
        setTimeout(() => {
          try {
            const maxY2 = Math.max(document.documentElement.scrollHeight || 0, document.body.scrollHeight || 0);
            const y2 = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
            if (y2 < maxY2 - 30) {
              window.scrollTo(0, maxY2);
              logRightSidebarDocker('bottom-realign', { from: y2, to: maxY2 });
            }
          } catch (e) {}
        }, 500);
      }
    }, 50);
    logRightSidebarDocker('scroll-attempt', {
      kind,
      method,
      beforeY,
      targetY,
      errorMessage: errorMessage || undefined,
    });
  }
  function openRightSidebarReplyWhenReady(retry = 0) {
    const hasForm = getRightSidebarReplyForm();
    logRightSidebarDocker('reply-click', {
      retry,
      hasForm: !!hasForm,
      formAction: hasForm ? (hasForm.getAttribute('action') || '') : '',
      readyState: document.readyState,
    });
    if (hasForm) {
      try {
        logRightSidebarDocker('reply-open-start', {
          retry,
          hasController: typeof ensureRightSidebarReplyController === 'function',
        });
        ensureRightSidebarReplyController().open();
        logRightSidebarDocker('reply-open-called', { retry, ok: true });
      } catch (err) {
        logRightSidebarDocker('reply-open-called', {
          retry,
          ok: false,
          errorMessage: err && err.message ? err.message : String(err),
          errorStack: err && err.stack ? String(err.stack).slice(0, 500) : undefined,
        });
        if (typeof toast === 'function') toast('打开回复浮窗失败,请查看控制台日志');
      }
      return;
    }
    if (retry < 12) {
      logRightSidebarDocker('reply-wait-form', { retry, nextRetryInMs: 80 });
      setTimeout(() => openRightSidebarReplyWhenReady(retry + 1), 80);
      return;
    }
    logRightSidebarDocker('reply-form-missing', {
      retry,
      replyFormCount: document.querySelectorAll('form[action="/Home/Forum/doReplyThread.html"]').length,
      postFormCount: document.querySelectorAll('#h-post-form form[action="/Home/Forum/doPostThread.html"]').length,
    });
    if (typeof toast === 'function') toast('未找到回复/发串表单');
  }
  function bindRightSidebarReplyButton(docker) {
    if (!docker || !docker.querySelector) {
      logRightSidebarDocker('reply-bind-skip', { reason: 'no-docker' });
      return;
    }
    const replyBtn = docker.querySelector('[data-type="REPLY"]');
    if (!replyBtn) {
      logRightSidebarDocker('reply-bind-skip', { reason: 'no-reply-btn' });
      return;
    }
    if (replyBtn.dataset.xdexReplyDockBound === '1') {
      logRightSidebarDocker('reply-bind-skip', { reason: 'already-bound' });
      return;
    }
    replyBtn.dataset.xdexReplyDockBound = '1';
    replyBtn.addEventListener('click', function onDockerReplyClick(e) {
      logRightSidebarDocker('reply-btn-event', {
        type: e && e.type,
        readyState: document.readyState,
      });
      openRightSidebarReplyWhenReady();
    });
    logRightSidebarDocker('reply-bind-ok', {
      bound: true,
      hasClickListenerMarker: replyBtn.dataset.xdexReplyDockBound === '1',
    });
  }
  function bindRightSidebarShellButtons(docker) {
    if (!docker) {
      logRightSidebarDocker('shell-bind-skip', { reason: 'no-docker' });
      return;
    }
    const supportsHasSelector = !!(window.CSS && CSS.supports && CSS.supports('selector(:has(*))'));
    const alreadyBound = docker.dataset.xdexSidebarShellBound === '1';
    if (!alreadyBound) {
      docker.dataset.xdexSidebarShellBound = '1';
      if (!supportsHasSelector) {
        docker.addEventListener('mouseenter', function onDockerEnter() {
          docker.classList.add('is-hover');
        });
        docker.addEventListener('mouseleave', function onDockerLeave() {
          docker.classList.remove('is-hover');
        });
      }
      const topBtn = docker.querySelector('[data-type="TOP"]');
      const bottomBtn = docker.querySelector('[data-type="BOTTOM"]');
      if (topBtn) {
        topBtn.addEventListener('click', function onDockerTopClick(e) {
          logRightSidebarDocker('top-btn-event', { type: e && e.type });
          scrollPageByDockerButton('TOP');
        });
      }
      if (bottomBtn) {
        bottomBtn.addEventListener('click', function onDockerBottomClick(e) {
          logRightSidebarDocker('bottom-btn-event', { type: e && e.type });
          scrollPageByDockerButton('BOTTOM');
        });
      }
      // 版块页/时间线页:上一串/下一串(REPLY 居中)
      const prevBtn = docker.querySelector('[data-type="PREV-THREAD"]');
      const nextBtn = docker.querySelector('[data-type="NEXT-THREAD"]');
      if (prevBtn) {
        prevBtn.addEventListener('click', function onDockerPrevThreadClick(e) {
          logRightSidebarDocker('prev-thread-btn-event', { type: e && e.type });
          scrollToAdjacentThread(-1);
        });
      }
      if (nextBtn) {
        nextBtn.addEventListener('click', function onDockerNextThreadClick(e) {
          logRightSidebarDocker('next-thread-btn-event', { type: e && e.type });
          scrollToAdjacentThread(1);
        });
      }
      logRightSidebarDocker('shell-bind-ok', {
        supportsHasSelector,
        hoverFallbackBound: !supportsHasSelector,
        topBound: !!topBtn,
        bottomBound: !!bottomBtn,
        prevThreadBound: !!prevBtn,
        nextThreadBound: !!nextBtn,
        topTag: topBtn ? topBtn.tagName : '',
        bottomTag: bottomBtn ? bottomBtn.tagName : '',
      });
    } else {
      logRightSidebarDocker('shell-bind-skip', {
        reason: 'already-bound',
        supportsHasSelector,
      });
    }
    bindRightSidebarReplyButton(docker);
  }
  function markRightSidebarDockerBootMotion(docker) {
    if (!docker || docker.dataset.xdexBootMotionDone === '1') return;
    docker.classList.add('xdex-docker-boot');
    const clear = () => {
      docker.classList.remove('xdex-docker-boot');
      docker.dataset.xdexBootMotionDone = '1';
    };
    // 覆盖 ready 同步 + batch1/2 高峰;之后恢复短过渡
    const timer = window.setTimeout(clear, 1500);
    docker.dataset.xdexBootMotionTimer = String(timer);
  }
  function scheduleReplyOverlayStylePrefetch() {
    if (scheduleReplyOverlayStylePrefetch.__scheduled) return;
    scheduleReplyOverlayStylePrefetch.__scheduled = true;
    const run = () => {
      try {
        if (!document.getElementById('qp-style') && typeof ensureReplyOverlayStyle === 'function') {
          ensureReplyOverlayStyle();
        }
        if (typeof ensureDarkReaderThemeObserver === 'function') ensureDarkReaderThemeObserver();
      } catch (e) {}
    };
    // 空闲时预热浮窗样式,避免 batch2 与首次悬停抢主线程;首次点 REPLY 仍有 open() 兜底
    if (typeof requestIdleCallback === 'function') {
      requestIdleCallback(() => run(), { timeout: 2500 });
    } else {
      window.setTimeout(run, 1800);
    }
  }
  function tryReplaceRightSidebarEarly() {
    if (!document.body) return false;
    ensureRightSidebarShellStyle();
    // 尽早同步 Dark Reader class:设置面板暗色依赖它,不能等 qp-style / batch2
    try { ensureDarkReaderThemeSync(); } catch (e) {}
    const nativeTool = document.querySelector('#h-tool');
    if (nativeTool) nativeTool.remove();
    let docker = document.querySelector('.hld__docker');
    if (!docker) {
      document.body.insertAdjacentHTML('beforeend', buildRightSidebarDockerHtml());
      docker = document.querySelector('.hld__docker');
      if (docker) docker.dataset.xdexEarlyDocker = '1';
    }
    if (docker) markRightSidebarDockerBootMotion(docker);
    // 按设置应用「固定/隐藏」模式(不触碰 :has 回退逻辑)
    if (docker) applyDockDisplayMode(getDockDisplayMode());
    bindRightSidebarShellButtons(docker);
    return !!docker;
  }
  function getDockDisplayMode() {
    try {
      const stateMode = SettingPanel && SettingPanel.state && SettingPanel.state.dockDisplayMode;
      if (stateMode === 'fixed' || stateMode === 'hover') return stateMode;
      const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      const m = cfg.dockDisplayMode;
      if (m === 'fixed' || m === 'hover') return m;
    } catch (e) {}
    return 'fixed';
  }
  function applyDockDisplayMode(mode) {
    const docker = document.querySelector('.hld__docker');
    if (!docker) return;
    const fixed = mode === 'fixed';
    docker.classList.toggle('xdex-dock-fixed', fixed);
    console.log('[rightSidebarDocker] 显示模式应用', { mode: fixed ? 'fixed' : 'hover' });
  }
  function isDarkReaderActive() {
    const root = document.documentElement;
    if (!root) return false;
    const mode = root.getAttribute('data-darkreader-mode');
    const scheme = root.getAttribute('data-darkreader-scheme');
    return !!(mode && mode !== 'off') || !!(scheme && scheme !== 'off');
  }
  function getReplyOverlayThemeTokens(dark) {
    // 用 !important 写回具体颜色,避免 Dark Reader 后续改写 CSS 变量 / 反色后边框跑偏。
    return dark ? {
      shellBg: '#2b2c2d',
      formBg: '#2b2c2d',
      previewBg: '#483327',
      textareaBg: '#1f2021',
      border: '#4b4d50',
      outline: '#3a3c3f',
      shadow: 'rgba(0,0,0,.55)',
      resetBg: 'rgba(19,20,21,.88)',
      resetColor: '#e8e6e3',
    } : {
      shellBg: '#FFFFEE',
      formBg: '#FFFFEE',
      previewBg: '#F0E0D6',
      textareaBg: '#fff',
      border: '#ccc',
      outline: '#fff',
      shadow: 'rgba(0,0,0,.24)',
      resetBg: 'rgba(0,0,0,.6)',
      resetColor: '#fff',
    };
  }
  // 自定义深色是否生效:仅当设置开启且 Dark Reader 未激活时
  function isCustomDarkActive() {
    try {
      if (isDarkReaderActive()) return false; // DR 激活 → 由 DR 定色,脚本深色停用
      return SettingPanel.state.enableCustomDarkTheme === 'dark';
    } catch (e) { return false; }
  }
  // 深色开关视觉:轨道白/黑、日/月图标切换、滑块位移
  function syncDarkToggleVisual() {
    const cb = document.getElementById('sp_enableCustomDarkTheme');
    const track = document.getElementById('sp_dark_toggle_track');
    const thumb = document.getElementById('sp_dark_toggle_thumb');
    const sun = document.getElementById('sp_dark_icon_sun');
    const moon = document.getElementById('sp_dark_icon_moon');
    if (!cb || !track || !thumb) return;
    const on = !!cb.checked;
    // 深色轨道用淡黑(#3a3a3a),比月亮图标的黑色浅、比滑块底深,保证月亮可读
    track.style.background = on ? '#3a3a3a' : '#fff';
    track.style.borderColor = on ? '#555' : '#ccc';
    thumb.style.background = on ? '#2b2b2b' : '#fff';
    thumb.style.transform = on ? 'translateX(22px)' : 'translateX(0)';
    if (sun) sun.style.display = on ? 'none' : '';
    if (moon) moon.style.display = on ? '' : 'none';
  }
  window.__xdexSyncDarkToggleVisual = syncDarkToggleVisual;
  // 整页深色同步:打 xdex-custom-dark 类控制覆盖样式;脚本 UI 沿用 xdex-darkreader-active 暗色规则
  function syncCustomPageTheme() {
    const root = document.documentElement;
    if (!root) return;
    // state 未就绪守卫:SettingPanel.init() 在 $(document).ready 里才填充 state,
    // 早于此的同步(tryReplaceRightSidebarEarly → ensureDarkReaderThemeSync)读到空 state
    // 会按 false 处理——摘掉 early 刚打的类并把 sessionStorage 毒化为 'false',
    // 下次刷新 early 判定即失败 → 开屏闪浅。守卫期间保持 early 注入状态并写入 'true'。
    if (!SettingPanel.state || !('enableCustomDarkTheme' in SettingPanel.state)) {
      try { sessionStorage.setItem(XDEX_DARK_SESSION_KEY, 'true'); } catch (e) {}
      return;
    }
    const on = isCustomDarkActive();
    console.log('[xdex-theme-sync] stateLoaded=' + ('enableCustomDarkTheme' in (SettingPanel.state || {})) + ' on=' + on + ' dr=' + isDarkReaderActive() + ' hadEarlyClass=' + root.classList.contains('xdex-custom-dark'));
    // 维护会话记忆:下次刷新 document-start 阶段同步可读,避免等异步 GM 存储造成闪浅
    try { sessionStorage.setItem(XDEX_DARK_SESSION_KEY, on ? 'true' : 'false'); } catch (e) {}
    root.classList.toggle('xdex-custom-dark', on);
    document.documentElement.style.colorScheme = (on || isDarkReaderActive()) ? 'dark' : 'light';
    if (on) {
      // early fallback 已完成首帧使命:完整规则接管后删除(避免两份样式冲突)
      const early = document.getElementById('xdex-custom-theme-early');
      if (early) early.remove();
      refreshCustomDarkThemeStyle();
      scheduleRefreshCustomDarkTheme(); // 首轮枚举完成后(含 fetch 兜底)再补一次完整表
    } else {
      const s = document.getElementById('xdex-custom-theme');
      if (s) s.textContent = '';
      const early = document.getElementById('xdex-custom-theme-early');
      if (early) early.remove();
    }
  }
  // —— 深色配色算法(方案C):不维护映射表,按 Dark Reader 规律做 HSL 变换 ——
  // 规律(由 DR 实测映射反推):色相保持;亮度反转并压缩(亮→暗乘 0.16~0.22,暗→亮乘 0.85);
  // 饱和度乘 0.8;低饱和(S<0.1)统一转 210° 微蓝灰(S≈0.02~0.08)
  function xdexHexToRgb(hex) {
    let h = String(hex || '').replace(/^#/, '');
    if (h.length === 3) h = h.split('').map((c) => c + c).join('');
    if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
    return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
  }
  function xdexRgbToHex(r, g, b) {
    const c = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0');
    return '#' + c(r) + c(g) + c(b);
  }
  function xdexRgbToHsl(r, g, b) {
    r /= 255; g /= 255; b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    const l = (max + min) / 2;
    if (max === min) return [0, 0, l];
    const d = max - min;
    const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
    let h;
    if (max === r) h = ((g - b) / d + (g < b ? 6 : 0));
    else if (max === g) h = (b - r) / d + 2;
    else h = (r - g) / d + 4;
    h /= 6;
    return [h, s, l];
  }
  function xdexHslToRgb(h, s, l) {
    if (s === 0) { const v = l * 255; return [v, v, v]; }
    const hue2 = (p, q, t) => {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    };
    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;
    return [hue2(p, q, h + 1 / 3) * 255, hue2(p, q, h) * 255, hue2(p, q, h - 1 / 3) * 255];
  }
  // 原色 → 深色主题色:亮变暗、暗变亮、饱和压缩、灰阶转微蓝灰
  function xdexDarkColor(hex) {
    const rgb = xdexHexToRgb(hex);
    if (!rgb) return null;
    const [h, s, l] = xdexRgbToHsl(rgb[0], rgb[1], rgb[2]);
    if (l > 0.93) {
      // 近白底(#ffe/#fff 页面底)→ 一律中性化为深底(DR 实测同样处理)
      const [r, g, b] = xdexHslToRgb(210 / 360, 0.02, 0.16);
      return xdexRgbToHex(r, g, b);
    }
    if (s < 0.1) {
      if (l >= 0.5 && l <= 0.78) {
        // 中灰(#999/#aaa 级)→ 保持中亮,仅转微蓝灰(DR 实测:#999→#a29b92,不反转为深底)
        const [r, g, b] = xdexHslToRgb(210 / 360, 0.07, Math.min(0.66, l + 0.02));
        return xdexRgbToHex(r, g, b);
      }
      // 其余灰阶 → 按原亮度提亮为蓝灰(DR 映射规律:#707070→#9c958b 是提亮而非压暗)
      const dark = l < 0.1 ? 0.07 : (l < 0.3 ? 0.35 : Math.min(0.72, l + 0.22));
      const [r, g, b] = xdexHslToRgb(210 / 360, 0.08, dark);
      return xdexRgbToHex(r, g, b);
    }
    // 亮度反转压缩:亮→暗区,暗→亮区
    let l2;
    if (l >= 0.5) l2 = 0.5 - (l - 0.5) * 0.62;   // 0.97→0.19、0.89→0.25、0.73→0.42
    else l2 = 0.5 + (0.5 - l) * 0.85;             // 0.25→0.69、0.37→0.56、0.43→0.62
    const s2 = Math.min(1, s * 0.8);
    const [r, g, b] = xdexHslToRgb(h, s2, l2);
    return xdexRgbToHex(r, g, b);
  }
  // 前景白化:正文类文字不保色相,向白灰收敛(主流深色主题策略,可读性优先)
  // l2 = 亮度反转后与目标白灰按比例混合;饱和度随混合比例同步衰减
  function xdexDarkForeground(hex) {
    const rgb = xdexHexToRgb(hex);
    if (!rgb) return null;
    const [h, s, l] = xdexRgbToHsl(rgb[0], rgb[1], rgb[2]);
    if (l > 0.93 || s < 0.1) return xdexDarkColor(hex); // 底色/灰阶走通用分支
    let l2 = l >= 0.5 ? 0.5 - (l - 0.5) * 0.62 : 0.5 + (0.5 - l) * 0.85;
    // 混入 88% 目标白灰 #d8d7d4(L≈0.85, S≈0.02),仅保留 12% 原反转结果(近乎纯白灰,粉调不可见)
    const mixL = l2 * 0.12 + 0.85 * 0.88;
    const mixS = s * 0.8 * 0.12;
    const [r, g, b] = xdexHslToRgb(h, mixS, mixL);
    return xdexRgbToHex(r, g, b);
  }
  // 页面原生样式表来源(与站点实际引用一致;uikit 为 CDN 时走 fetch 兜底)
  const XDEX_DARK_THEME_SHEET_URLS = [
    location.origin + '/Public/Css/h.desktop.css',
    'https://www.nmbxd1.com/Public/Css/h.desktop.css'
  ];
  // 从 CSS 文本收集颜色值并生成 :root.xdex-custom-dark 全局反转规则
  function xdexCollectColorsFromCssText(cssText, colorSet) {
    if (!cssText) return;
    const re = /(#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b|rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(?:,\s*[\d.]+)?\s*\))/g;
    let m;
    while ((m = re.exec(cssText))) {
      const token = m[1];
      if (token.startsWith('#')) colorSet.add(token.toLowerCase());
      else {
        const nums = token.match(/\d+/g);
        if (nums && nums.length >= 3) {
          colorSet.add(xdexRgbToHex(+nums[0], +nums[1], +nums[2]).toLowerCase());
        }
      }
    }
  }
  // 生成全局色彩反转规则:对页面全部颜色类做类名切换(性能远优于逐选择器覆盖)
  const xdexDarkCache = new Map();
  function xdexToDarkCached(hex) {
    if (!xdexDarkCache.has(hex)) xdexDarkCache.set(hex, xdexDarkColor(hex) || hex);
    return xdexDarkCache.get(hex);
  }
  function buildCustomDarkThemeStyleText(colorSet) {
    const D = (hex) => xdexToDarkCached(hex);
    const F = (hex) => xdexDarkForeground(hex) || D(hex); // 前景白化(正文可读性优先)
    const lines = [
      '/* generated by xdex custom dark (algorithm-based) */',
      ':root.xdex-custom-dark { color-scheme: dark; }',
      // 页面底与正文:正文文字走前景白化(可读性优先),底色走通用反转
      // 注意::root 即 html 本身,":root.xdex-custom-dark html" 是永不匹配的死选择器
      'html.xdex-custom-dark, html.xdex-custom-dark body { background-color: ' + D('#ffe') + ' !important; color: ' + F('#800000') + ' !important; }',
      // 左侧菜单(不用白化:保留原反转色彩)
      ':root.xdex-custom-dark #h-menu { background-color: ' + D('#fff') + ' !important; color: ' + D('#c00') + ' !important; border-right-color: ' + D('#000') + ' !important; }',
      ':root.xdex-custom-dark #h-menu #h-menu-content .h-nav-parent-header, :root.xdex-custom-dark #h-menu #h-menu-content .h-nav-header { color: ' + D('#c00') + ' !important; }',
      ':root.xdex-custom-dark #h-menu #h-menu-content .h-active { background: ' + D('#ea8') + ' !important; }',
      ':root.xdex-custom-dark #h-menu #h-menu-content .h-active a { color: ' + D('#800000') + ' !important; }',
      ':root.xdex-custom-dark #h-menu #h-menu-content .h-nav-item a { color: ' + D('#07d') + ' !important; }',
      ':root.xdex-custom-dark #h-menu #h-menu-content .h-nav-item:hover a { color: ' + D('#059') + ' !important; }',
      // 底部导航与工具
      ':root.xdex-custom-dark #h-bottom-nav { background: ' + D('#fff') + ' !important; border-top-color: ' + D('#000') + ' !important; }',
      ':root.xdex-custom-dark #h-tool .h-tool-btn { background: ' + D('#b00') + ' !important; }',
      // 正文信息行:标题/邮箱保色相(功能性红绿);info 通用通配不再扫掉彩色(红字绿字变灰的根因)
      ':root.xdex-custom-dark #h-content h2.h-title, :root.xdex-custom-dark .h-threads-item .h-threads-info .h-threads-info-title, :root.xdex-custom-dark .h-threads-info .h-threads-info-title { color: ' + D('#cc1105') + ' !important; }',
      ':root.xdex-custom-dark .h-threads-item .h-threads-info .h-threads-info-email, :root.xdex-custom-dark .h-threads-info .h-threads-info-email { color: ' + D('#117743') + ' !important; }',
      // SAGE 警示行(uk-text-danger 原生红)保持警示红,不走 tips 灰
      ':root.xdex-custom-dark .h-threads-item .h-threads-tips.uk-text-danger, :root.xdex-custom-dark .h-threads-tips.uk-text-danger { color: ' + D('#cc1105') + ' !important; }',
      ':root.xdex-custom-dark .h-threads-item .h-threads-tips, :root.xdex-custom-dark .h-threads-tips { color: ' + F('#707070') + ' !important; }',
      // 回复块
      ':root.xdex-custom-dark .h-threads-item .h-threads-item-replies .h-threads-item-reply .h-threads-item-reply-main { background: ' + D('#f0e0d6') + ' !important; }',
      // 信息行链接与正文链接(No.xxx/举报/回应/订阅/外部链接):uikit 全局 a 色 #1e87f0 落在深底上偏暗,
      // 统一提亮至与分页/浮窗一致的亮蓝(DR 级观感)
      ':root.xdex-custom-dark .h-threads-item .h-threads-info a, :root.xdex-custom-dark .h-threads-content a { color: ' + D('#07d') + ' !important; }',
      // PO主标记(uk-text-primary,uikit 主题蓝):同样提亮
      ':root.xdex-custom-dark .h-threads-item .uk-text-primary { color: ' + D('#07d') + ' !important; }',
      // 分页
      ':root.xdex-custom-dark .h-pagination li a, :root.xdex-custom-dark .h-pagination li span { color: ' + D('#07d') + ' !important; background: ' + D('#fff') + ' !important; }',
      ':root.xdex-custom-dark .h-pagination li:hover a { color: ' + D('#059') + ' !important; }',
      ':root.xdex-custom-dark .h-pagination li.uk-active a, :root.xdex-custom-dark .h-pagination li.uk-active span { color: ' + F('#800000') + ' !important; background: ' + D('#b00') + ' !important; }',
      // 引用号
      ':root.xdex-custom-dark font[color="#789922"] { color: ' + D('#789922') + ' !important; }',
      // 隐藏文本
      ':root.xdex-custom-dark .h-hidden-text { background: ' + D('#666') + ' !important; color: transparent !important; }',
      ':root.xdex-custom-dark .h-hidden-text:hover { background: transparent !important; color: ' + F('#800000') + ' !important; }',
      // 原生引用浮窗与发帖表单标题栏
      ':root.xdex-custom-dark #h-ref-view .h-threads-item-ref, :root.xdex-custom-dark #h-ref-view .uk-container { background: ' + D('#f0e0d6') + ' !important; }',
      ':root.xdex-custom-dark #h-post-form .h-post-form-title { background: ' + D('#ea8') + ' !important; }',
      // —— 脚本自建 UI 深色(修复覆盖不足):通用前景/背景反转 ——
      ':root.xdex-custom-dark .qp-quote { color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark .qp-header .qp-level, :root.xdex-custom-dark .qp-header .qp-back, :root.xdex-custom-dark .qp-header .qp-jump { color: ' + F('#800000') + ' !important; }',
      // 颜文字面板
      ':root.xdex-custom-dark .kaomoji-panel, :root.xdex-custom-dark .kaomoji-popup, :root.xdex-custom-dark .kaomoji-dropdown { background: ' + D('#fff') + ' !important; color: ' + F('#800000') + ' !important; border-color: ' + D('#000') + ' !important; }',
      ':root.xdex-custom-dark .kaomoji-panel *, :root.xdex-custom-dark .kaomoji-popup *, :root.xdex-custom-dark .kaomoji-dropdown * { color: ' + F('#800000') + ' !important; }',
      // 回复浮窗内的原生片段:默认白化,但功能性彩色(title/email/引用/链接/SAGE)保色相
      ':root.xdex-custom-dark .qp-body .h-threads-info, :root.xdex-custom-dark .qp-body .h-threads-info *, :root.xdex-custom-dark .qp-body .h-threads-content, :root.xdex-custom-dark .qp-body .h-threads-content * { color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark .qp-body .h-threads-info-title { color: ' + D('#cc1105') + ' !important; }',
      ':root.xdex-custom-dark .qp-body .h-threads-info-email { color: ' + D('#117743') + ' !important; }',
      ':root.xdex-custom-dark .qp-body .h-threads-tips.uk-text-danger { color: ' + D('#cc1105') + ' !important; }',
      ':root.xdex-custom-dark .qp-body .h-threads-info a, :root.xdex-custom-dark .qp-body .h-threads-content a { color: ' + D('#07d') + ' !important; }',
      ':root.xdex-custom-dark .qp-body { background: ' + D('#ffe') + ' !important; }',
      // 预览框(发送前实时预览):与回复块完全同构 —— 本体兜底深棕底+浅字,
      // info/content 白化后跟保色规则(链接/PO主/引用绿),特异性白化规则更高
      ':root.xdex-custom-dark .h-preview-box { background: ' + D('#f0e0d6') + ' !important; color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark .h-preview-box .h-threads-item-reply-main { background: ' + D('#f0e0d6') + ' !important; }',
      ':root.xdex-custom-dark .h-preview-box .h-threads-info-title { color: ' + D('#cc1105') + ' !important; }',
      ':root.xdex-custom-dark .h-preview-box .h-threads-info-email { color: ' + D('#117743') + ' !important; }',
      ':root.xdex-custom-dark .h-preview-box .h-threads-info, :root.xdex-custom-dark .h-preview-box .h-threads-info *, :root.xdex-custom-dark .h-preview-box .h-threads-content, :root.xdex-custom-dark .h-preview-box .h-threads-content * { color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark .h-preview-box font[color="#789922"] { color: ' + D('#789922') + ' !important; }',
      ':root.xdex-custom-dark .h-preview-box .h-threads-info a, :root.xdex-custom-dark .h-preview-box .h-threads-content a { color: ' + D('#07d') + ' !important; }',
      ':root.xdex-custom-dark .h-preview-box .uk-text-primary { color: ' + D('#07d') + ' !important; }',
      ':root.xdex-custom-dark #cookie-confirm-modal { color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark #cookie-confirm-modal h3 { color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark #cookie-confirm-modal p { color: ' + F('#707070') + ' !important; }',
      ':root.xdex-custom-dark #cookie-confirm-modal p font[color="#789922"] { color: ' + D('#789922') + ' !important; }',
      ':root.xdex-custom-dark #cookie-confirm-list [data-cookie-id] { color: ' + F('#800000') + ' !important; background-color: ' + D('#fafafa') + ' !important; }',
      ':root.xdex-custom-dark #cookie-confirm-list .h-threads-info-uid { color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark #cookie-confirm-actions button { background: ' + D('#fafafa') + ' !important; color: ' + F('#800000') + ' !important; border: 1px solid ' + D('#bbb') + ' !important; border-radius: 6px; cursor: pointer; }',
      // 设置面板分组输入行(标记/屏蔽/只看/常用串/订阅/关键词):容器去白底、输入框深底浅字、徽标/删除钮暗底
      ':root.xdex-custom-dark #marked-inputs-container > div > div, :root.xdex-custom-dark #blocked-inputs-container > div > div, :root.xdex-custom-dark #favorite-thread-inputs-container > div > div, :root.xdex-custom-dark #subscription-feed-inputs-container > div > div, :root.xdex-custom-dark #thread-cookie-whitelist-inputs-container > div > div, :root.xdex-custom-dark #blocked-keyword-inputs-container > div > div { background: ' + D('#fafafa') + ' !important; border-color: ' + D('#a98f7a') + ' !important; }',
      // input 深底浅字,但排除 xdex-switch(正则开关保红/蓝语义)
      ':root.xdex-custom-dark #marked-inputs-container input:not(.xdex-switch), :root.xdex-custom-dark #blocked-inputs-container input:not(.xdex-switch), :root.xdex-custom-dark #favorite-thread-inputs-container input:not(.xdex-switch), :root.xdex-custom-dark #subscription-feed-inputs-container input:not(.xdex-switch), :root.xdex-custom-dark #thread-cookie-whitelist-inputs-container input:not(.xdex-switch), :root.xdex-custom-dark #blocked-keyword-inputs-container input:not(.xdex-switch), :root.xdex-custom-dark #blocked-keyword-inputs-container select { background: ' + D('#fff') + ' !important; color: ' + F('#800000') + ' !important; border-color: ' + D('#bbb') + ' !important; }',
      // button 深底浅字,但排除 marked-color-swatch(保持用户选定的标记色)
      ':root.xdex-custom-dark #marked-inputs-container button:not(.marked-color-swatch), :root.xdex-custom-dark #blocked-inputs-container button, :root.xdex-custom-dark #favorite-thread-inputs-container button, :root.xdex-custom-dark #subscription-feed-inputs-container button, :root.xdex-custom-dark #thread-cookie-whitelist-inputs-container button, :root.xdex-custom-dark #blocked-keyword-inputs-container button { background: ' + D('#fafafa') + ' !important; color: ' + F('#800000') + ' !important; border-color: ' + D('#a98f7a') + ' !important; }',
      ':root.xdex-custom-dark #marked-inputs-container span, :root.xdex-custom-dark #blocked-inputs-container span, :root.xdex-custom-dark #favorite-thread-inputs-container span, :root.xdex-custom-dark #subscription-feed-inputs-container span, :root.xdex-custom-dark #thread-cookie-whitelist-inputs-container span, :root.xdex-custom-dark #blocked-keyword-inputs-container span { background: ' + D('#fafafa') + ' !important; color: ' + F('#800000') + ' !important; }',
      // 常用串菜单(脚本自建侧栏)
      ':root.xdex-custom-dark .xdex-fav-threads-menu, :root.xdex-custom-dark .xdex-fav-threads-menu * { background-color: ' + D('#fff') + ' !important; color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark .xdex-fav-threads-menu .h-active, :root.xdex-custom-dark .xdex-fav-threads-menu .h-active * { background: ' + D('#ea8') + ' !important; }',
      // 饼干切换模块:当前饼干名白色字体(含异常红色状态排除)
      ':root.xdex-custom-dark #current-cookie-display { color: #d8d7d4 !important; }',
      ':root.xdex-custom-dark #current-cookie-display[style*="red"], :root.xdex-custom-dark #current-cookie-display[style*="rgb(255, 0, 0)"] { color: #e85248 !important; }',
      // 折叠占位按钮(发送表单可选项/关键词/饼干屏蔽折叠区通用)
      ':root.xdex-custom-dark .xdex-placeholder.xdex-generic-toggle, :root.xdex-custom-dark .xdex-placeholder { background: ' + D('#fafafa') + ' !important; color: ' + F('#888') + ' !important; border-color: ' + D('#bbb') + ' !important; }',
      // 外部图床图片预览折叠容器(.injected-image-container / .iic-*):内联浅灰系,深色下整块刺眼
      ':root.xdex-custom-dark .injected-image-container { background: ' + D('#f9f9f9') + ' !important; border-color: ' + D('#ddd') + ' !important; }',
      ':root.xdex-custom-dark .iic-header { background: ' + D('#f2f2f2') + ' !important; border-bottom-color: ' + D('#e6e6e6') + ' !important; }',
      ':root.xdex-custom-dark .iic-title { color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark .iic-footer { background: ' + D('#f9f9f9') + ' !important; border-top-color: ' + D('#eee') + ' !important; }',
      ':root.xdex-custom-dark .iic-more-btn-top, :root.xdex-custom-dark .iic-toggle-btn, :root.xdex-custom-dark .iic-more-btn-bottom { background: ' + D('#fff') + ' !important; color: ' + F('#800000') + ' !important; border-color: ' + D('#ccc') + ' !important; }',
      ':root.xdex-custom-dark .iic-body img { border-color: ' + D('#ccc') + ' !important; }',
      ':root.xdex-custom-dark .iic-body > div { color: ' + F('#707070') + ' !important; }',
      // 颜文字选择框:原生 select 亮白 → 深色
      ':root.xdex-custom-dark #h-emot-select { background: ' + D('#fff') + ' !important; color: ' + F('#800000') + ' !important; border-color: ' + D('#bbb') + ' !important; }',
      ':root.xdex-custom-dark #h-emot-select option { background: ' + D('#fff') + ' !important; color: ' + F('#800000') + ' !important; }',
      // 颜文字触发按钮与键盘选中项
      ':root.xdex-custom-dark .kaomoji-trigger { background: ' + D('#fafafa') + ' !important; color: ' + F('#800000') + ' !important; border-color: ' + D('#bbb') + ' !important; }',
      ':root.xdex-custom-dark .kaomoji-item.kaomoji-active { background: #3a3d40 !important; }',
      ':root.xdex-custom-dark .kaomoji-item { color: ' + F('#800000') + ' !important; }',
      // 悬浮底色:压过浅色源的 #f2f2f2,用深色中灰 + 白化前景
      ':root.xdex-custom-dark .kaomoji-item:hover { background: #3a3d40 !important; }',
      // 回应省略提示(warn_txt2):站点原生 .warn_txt2 直接命中子元素,需单独白化(容器继承管不到)
      ':root.xdex-custom-dark .warn_txt2 { color: ' + F('#707070') + ' !important; }',
      ':root.xdex-custom-dark .kaomoji-item { color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark .kaomoji-quick-sort, :root.xdex-custom-dark #h-emot-select ~ select { background: ' + D('#fff') + ' !important; color: ' + F('#800000') + ' !important; }',
      ':root.xdex-custom-dark html, :root.xdex-custom-dark body, :root.xdex-custom-dark #h-menu, :root.xdex-custom-dark .h-threads-item, :root.xdex-custom-dark .h-preview-box { transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease; }',
      'html.xdex-theme-anim, html.xdex-theme-anim body, html.xdex-theme-anim #h-menu, html.xdex-theme-anim #h-content, html.xdex-theme-anim .h-threads-item, html.xdex-theme-anim .h-threads-item *, html.xdex-theme-anim .h-preview-box { transition: background-color 0.35s ease, color 0.35s ease, border-color 0.35s ease; }'
    ];
    return lines.join('\n');
  }
  function ensureCustomDarkThemeStyle(colorSet) {
    const existing = document.getElementById('xdex-custom-theme');
    const text = buildCustomDarkThemeStyleText(colorSet || new Set());
    if (existing) { existing.textContent = text; return existing; }
    const style = document.createElement('style');
    style.id = 'xdex-custom-theme';
    style.textContent = text;
    document.head.appendChild(style);
    return style;
  }
  // —— 样式表枚举:从页面原生 CSS 收集全部颜色,生成“原色→深色”CSS 变量表 + 通用覆盖 ——
  function xdexCollectColorsFromStyleSheets(colorSet) {
    try {
      for (const sheet of document.styleSheets) {
        let rules = null;
        try { rules = sheet.cssRules; } catch (e) { /* 跨域样式表:cssRules 受限,走 fetch 兜底 */ }
        if (rules) {
          const drain = (list) => {
            for (const rule of list) {
              if (rule.type === 1 /* STYLE_RULE */ && rule.style) {
                for (let i = 0; i < rule.style.length; i++) {
                  const prop = rule.style[i];
                  if (!/color|background|border|outline|shadow|fill|stroke/i.test(prop)) continue;
                  const val = rule.style.getPropertyValue(prop);
                  xdexCollectColorsFromCssText(val, colorSet);
                }
              } else if (rule.cssRules) {
                drain(rule.cssRules);
              }
            }
          };
          drain(rules);
        } else if (sheet.href && !colorSet.__fetched) {
          // 跨域表:异步 fetch 后重建样式(不阻塞首次注入)
          colorSet.__fetched = true;
          fetch(sheet.href).then((r) => r.text()).then((txt) => {
            const s2 = new Set();
            xdexCollectColorsFromCssText(txt, s2);
            refreshCustomDarkThemeStyle(s2);
          }).catch(() => {});
        }
      }
    } catch (e) {}
  }
  // 生成“颜色变量表 + 按值替换”的通用覆盖:用 CSS 变量把每个原色挂到 :root,
  // 再通过 attribute/class 无关的全局选择器无法按值匹配,故这里输出【每色一类】的变量表,
  // 真正的按值替换交给 styleOverridesText(对常见结构性选择器做深色赋值)。
  function xdexDarkVarsText(colorSet) {
    const out = [];
    for (const hex of colorSet) {
      const dark = xdexToDarkCached(hex);
      if (!dark || dark === hex) continue;
      const varName = '--xdex-dark-' + hex.replace('#', '');
      out.push(varName + ': ' + dark + ';');
    }
    return out.length ? ':root.xdex-custom-dark {\n  ' + out.join('\n  ') + '\n}' : '';
  }
  let refreshTimer = 0;
  function refreshCustomDarkThemeStyle(extraColors) {
    const colorSet = new Set(extraColors || []);
    xdexCollectColorsFromStyleSheets(colorSet);
    // 兜底核心色(防止样式表枚举失败时页面全白)
    ['#ffe', '#fff', '#800000', '#789922', '#f0e0d6', '#07d', '#c00', '#000', '#059', '#ea8', '#999', '#cc1105', '#117743', '#b00', '#707070', '#666'].forEach((c) => colorSet.add(c));
    const style = ensureCustomDarkThemeStyle(colorSet);
    const varsText = xdexDarkVarsText(colorSet);
    style.textContent = buildCustomDarkThemeStyleText(colorSet) + '\n' + varsText;
  }
  function scheduleRefreshCustomDarkTheme() {
    if (refreshTimer) return;
    refreshTimer = setTimeout(() => {
      refreshTimer = 0;
      refreshCustomDarkThemeStyle();
    }, 400);
  }
  function syncQuotePopupTheme() {
    const root = document.documentElement;
    if (!root) return;
    const dark = isDarkReaderActive();
    // 脚本 UI 暗色:DR 激活 或 自定义深色开启(复用现有 :root.xdex-darkreader-active 全部暗色规则)
    const uiDark = dark || isCustomDarkActive();
    root.classList.toggle('xdex-darkreader-active', uiDark);
    syncCustomPageTheme();
    // DR 激活时内联 !important 钉死必须退让:DR 反转后会重写这些属性,脚本钉死即优先级反超
    if (dark) {
      document.querySelectorAll('.qp-quote, .qp-body .qp-content-wrap, .qp-body .qp-content-wrap form, .qp-body .qp-content-wrap textarea[name="content"], .qp-reset-btn').forEach((el) => {
        ['background', 'border-color', 'outline-color', 'box-shadow', 'color'].forEach((p) => { try { el.style.removeProperty(p); } catch (e) {} });
      });
    }
    const theme = getReplyOverlayThemeTokens(uiDark);
    // CSS 变量兜底(未打开浮窗时也保持一致)
    root.style.setProperty('--xdex-qp-shell-bg', theme.shellBg);
    root.style.setProperty('--xdex-qp-form-bg', theme.formBg);
    root.style.setProperty('--xdex-qp-preview-bg', theme.previewBg);
    root.style.setProperty('--xdex-qp-textarea-bg', theme.textareaBg);
    root.style.setProperty('--xdex-qp-border', theme.border);
    root.style.setProperty('--xdex-qp-outline', theme.outline);
    root.style.setProperty('--xdex-qp-shadow', theme.shadow);
    root.style.setProperty('--xdex-qp-reset-bg', theme.resetBg);
    root.style.setProperty('--xdex-qp-reset-color', theme.resetColor);
    // 浮窗壳/边框/描边:Dark Reader 后处理常会改 border-color,必须 !important 钉死
    document.querySelectorAll('.qp-quote').forEach((el) => {
      el.style.setProperty('background', theme.shellBg, 'important');
      el.style.setProperty('border-color', theme.border, 'important');
      el.style.setProperty('outline-color', theme.outline, 'important');
      el.style.setProperty('box-shadow', `0 8px 24px ${theme.shadow}`, 'important');
    });
    document.querySelectorAll('.qp-body .qp-content-wrap, .qp-body .qp-content-wrap form').forEach((el) => {
      el.style.setProperty('background', theme.formBg, 'important');
    });
    document.querySelectorAll('.qp-body .qp-content-wrap textarea[name="content"]').forEach((el) => {
      el.style.setProperty('background', theme.textareaBg, 'important');
    });
    document.querySelectorAll('.qp-reset-btn, .qp-close-all').forEach((el) => {
      // 写 CSS 变量而非内联 color/background:内联 !important 会压过样式表 hover 规则,
      // 导致悬浮时只有边框变绿、svg(currentColor) 不变色
      el.style.setProperty('--xdex-qp-reset-bg', theme.resetBg);
      el.style.setProperty('--xdex-qp-reset-color', theme.resetColor);
      el.style.setProperty('--xdex-qp-reset-border', theme.resetBg);
    });
    document.querySelectorAll('.qp-body .qp-content-wrap .h-preview-box, .qp-body .qp-content-wrap .h-preview-box .h-threads-item, .qp-body .qp-content-wrap .h-preview-box .h-threads-item-replies, .qp-body .qp-content-wrap .h-preview-box .h-threads-item-reply, .qp-body .qp-content-wrap .h-preview-box .h-threads-item-reply-main').forEach((el) => {
      el.style.setProperty('background', theme.previewBg, 'important');
    });
  }
  // early open 也要能监听 Dark Reader 后续属性变化;只盯 html 上的 darkreader 属性,避免 subtree style 风暴。
  function ensureDarkReaderThemeObserver() {
    if (window.__xdexDarkReaderThemeObserver) return window.__xdexDarkReaderThemeObserver;
    let scheduled = 0;
    const scheduleSync = () => {
      if (scheduled) return;
      scheduled = requestAnimationFrame(() => {
        scheduled = 0;
        syncQuotePopupTheme();
      });
    };
    const observer = new MutationObserver(scheduleSync);
    // 先挂到 window 再 observe:early open 复用同一实例;赋值与 attributeFilter 保持相邻(契约测试就近断言)
    window.__xdexDarkReaderThemeObserver = observer;
    observer.observe(document.documentElement, {
      attributes: true,
      // DR 实际写入含 fr-init-once 与 style 内联变量(实测)→ 必须盯,否则开 DR 后仍浅色
      attributeFilter: ['data-darkreader-mode', 'data-darkreader-scheme', 'fr-init-once', 'style', 'class'],
    });
    return observer;
  }
  // 设置面板/登录框/历史等共用 :root.xdex-darkreader-active。
  // 主题同步必须独立于 qp-style:batch2 可延后浮窗大 CSS,但不能延后这个 class。
  function ensureDarkReaderThemeSync() {
    window.__xdexSyncDarkReaderTheme = syncQuotePopupTheme;
    ensureDarkReaderThemeObserver();
    syncQuotePopupTheme();
  }
  // early open 也必须立刻注入浮窗样式;不能等 batch2 的 replaceRightSidebar。
  // 否则早期点 REPLY 会出现:表单先裸奔到左上角 → 浮窗壳后到 → 再被样式/暗色主题二次重绘。
  function ensureReplyOverlayStyle() {
    // 始终刷新样式文本:脚本热更新后若只判断 id 存在,会继续用旧 CSS(如 height:80vh)
    let style = document.getElementById('qp-style');

    const styleExisted = !!style;
    if (!style) {
      style = document.createElement('style');
      style.id = 'qp-style';
    }
    {
      style.textContent = `
          :root {
            --xdex-qp-shell-bg: #FFFFEE;
            --xdex-qp-form-bg: #FFFFEE;
            --xdex-qp-preview-bg: #F0E0D6;
            --xdex-qp-textarea-bg: #fff;
            --xdex-qp-border: #ccc;
            --xdex-qp-outline: #fff;
            --xdex-qp-shadow: rgba(0,0,0,.24);
            --xdex-qp-reset-bg: rgba(0,0,0,.6);
            --xdex-qp-reset-color: #fff;
          }
          :root.xdex-darkreader-active {
            --xdex-qp-shell-bg: #2b2c2d;
            --xdex-qp-form-bg: #2b2c2d;
            --xdex-qp-preview-bg: #483327;
            --xdex-qp-textarea-bg: #1f2021;
            --xdex-qp-border: #4b4d50;
            --xdex-qp-outline: #3a3c3f;
            --xdex-qp-shadow: rgba(0,0,0,.55);
            --xdex-qp-reset-bg: rgba(19,20,21,.88);
            --xdex-qp-reset-color: #e8e6e3;
          }
          .qp-overlay {
            position: fixed; inset: 0; z-index: 9000;
            background: rgba(0,0,0,.45); display: none;
          }
          /* 仅回复浮窗:引用弹窗也用 .qp-stack,不能共用 height/overflow */
          .qp-overlay > .qp-stack {
            position: fixed; top: 50%; left: 50%;
            transform: translate(-50%, -50%);
            width: min(90vw, 820px);   /* 初始宽度为视口的90%,最大820px */
            min-width: 0;              /* 不设固定值,由 JS 动态控制 */
            max-width: none;           /* 允许用户手动扩展 */
            /* 默认不锁 80vh:由内容贴合,避免表单与预览之间被撑出大空隙 */
            height: auto;
            max-height: min(80vh, calc(100vh - 40px));
            overflow: visible; box-sizing: border-box;
          }
          /* 仅作用于回复浮窗壳:不要污染引用弹窗的 .qp-quote */
          .qp-overlay .qp-stack > .qp-quote {
            position: absolute;
            top: 0; left: 0;
            width: 100%;
            height: 100%;
            max-height: 100%;
            /* 自动贴高默认隐藏滚动条;达上限/手动缩放后再滚动,避免预览空→有时右侧滚动条闪一下 */
            overflow-x: hidden;
            overflow-y: hidden;
            background: var(--xdex-qp-shell-bg);
            border: 1px solid var(--xdex-qp-border);
            outline: 2px solid var(--xdex-qp-outline);
            border-radius: 8px;
            box-shadow: 0 8px 24px var(--xdex-qp-shadow);
            padding: 18px 20px 20px;
            box-sizing: border-box;
            display: flex;
            flex-direction: column;
          }
          .qp-overlay > .qp-stack.qp-user-sized > .qp-quote,
          .qp-overlay > .qp-stack.qp-content-scroll > .qp-quote {
            overflow-y: auto;
          }
          .qp-overlay > .qp-stack.qp-content-scroll > .qp-quote {
            scrollbar-width: thin;
          }
          .qp-overlay > .qp-stack.qp-content-scroll > .qp-quote::-webkit-scrollbar {
            width: 8px;
          }
          .qp-overlay .qp-stack > .qp-quote textarea[name="content"] {
            resize: vertical !important; /* 允许纵向缩放;宽度跟浮窗走 */
            min-width: 0 !important;
            width: 100% !important;
            max-width: none;
            box-sizing: border-box;
            flex: none !important; /* 不要被 flex 拉高造成假空隙 */
          }
          /* 浮窗内 textarea:允许纵向缩放 */
          .qp-body .h-post-form-textarea {
              resize: vertical !important;
              min-width: 0;
              max-width: 100%;
              box-sizing: border-box;
              flex: none !important;
          }
          /* 四边中段:移动浮窗(角部留给缩放) */
          .qp-drag-edge {
            position: absolute;
            pointer-events: auto;
            z-index: 3;
            cursor: move;
          }
          .qp-drag-edge.top    { top: 0;    left: 14px; right: 14px; height: 10px; }
          .qp-drag-edge.bottom { bottom: 0; left: 14px; right: 14px; height: 10px; }
          .qp-drag-edge.left   { top: 14px; bottom: 14px; left: 0;   width: 10px; }
          .qp-drag-edge.right  { top: 14px; bottom: 14px; right: 0;  width: 10px; }
          /* 四角:拉伸浮窗 */
          .qp-resize-corner {
            position: absolute;
            width: 14px;
            height: 14px;
            z-index: 5;
            pointer-events: auto;
            background: transparent;
          }
          .qp-resize-corner.nw { top: 0; left: 0; cursor: nwse-resize; }
          .qp-resize-corner.ne { top: 0; right: 0; cursor: nesw-resize; }
          .qp-resize-corner.sw { bottom: 0; left: 0; cursor: nesw-resize; }
          .qp-resize-corner.se { bottom: 0; right: 0; cursor: nwse-resize; }
          .qp-overlay .qp-quote.is-dragging,
          .qp-overlay .qp-quote.is-resizing { user-select: none !important; }
          .qp-overlay .qp-quote.is-dragging { cursor: grabbing !important; }
          .qp-body {
            flex: 0 0 auto; /* 贴内容高度,达上限后由 .qp-quote 整体滚动 */
            min-height: 0;
          }
          /* 归位按钮 */
          .qp-reset-btn {
            position: fixed; right: 12px; bottom: 12px;
            width: 30px; height: 30px; padding: 0;
            display: none;
            align-items: center; justify-content: center;
            color: var(--xdex-qp-reset-color); background: var(--xdex-qp-reset-bg);
            border: 1px solid rgba(255,255,255,.35); border-radius: 8px; cursor: pointer;
            z-index: 9001; /* 比 overlay 高 */
            user-select: none;
          }
          .qp-body .qp-content-wrap {
            display: flex;
            flex-direction: column;
            gap: 10px;
            max-width: none;       /* 改为 none,不限制最大宽度 */
            margin: 0;
            background: var(--xdex-qp-form-bg);
            /* 默认贴内容高度;只有手动拉高浮窗时才填满可用空间 */
            flex: 0 0 auto;
            min-height: 0;
            width: 100%;
            height: auto;
            overflow: visible; /* 不裁剪内容;超长时由回复浮窗 .qp-quote 整体滚动 */
          }
          .qp-body .qp-content-wrap form {
            max-width: 100%;
            box-sizing: border-box;
            background: var(--xdex-qp-form-bg);
            display: block; /* 保持站点原生表单流,避免 flex 把内部行拉开 */
            flex: 0 0 auto; /* 表单按内容高度,不吃掉空隙、也不被压扁 */
            min-height: auto;
            order: 0;
            position: relative;
            z-index: 2; /* 缩小空间时表单盖在预览上方,避免被盖住 */
          }
          /* textarea 宽度跟浮窗走,允许纵向缩放;高度由内容/用户拖拽决定,不 flex 撑满 */
          .qp-body .qp-content-wrap textarea[name="content"] {
            resize: vertical !important;
            min-width: 0 !important;
            min-height: 120px;
            width: 100% !important;
            max-width: none;
            flex: none !important;
            box-sizing: border-box;
            background: var(--xdex-qp-textarea-bg);
          }
          .qp-body .qp-content-wrap .h-preview-box {
            width: 100% !important;   /* 始终和容器一致 */
            overflow-wrap: break-word; /* 长单词/长链接换行 */
            word-break: break-word;    /* 兼容性处理 */
            white-space: normal;       /* 允许正常换行 */
            /* 关键:预览默认不 flex 撑满,否则会在表单下方制造大空隙 */
            flex: 0 1 auto;
            min-height: 0;
            max-height: none;
            overflow: visible; /* 预览不单独滚动;超长时由回复浮窗整体滚动 */
            order: 1;
            position: relative;
            z-index: 1;
          }
          /* 用户手动拉高浮窗后:允许 wrap/预览吃掉剩余高度 */
          /* 用户手动拉高:仅让表单区保持自然高度;内容超高时仍走整窗滚动 */
          .qp-overlay > .qp-stack.qp-user-sized .qp-content-wrap {
            flex: 0 0 auto;
            height: auto;
          }
          .qp-overlay > .qp-stack.qp-user-sized .qp-content-wrap .h-preview-box {
            flex: 0 0 auto;
            overflow: visible;
          }
          /* 兜底:引用弹窗保持内容自适应,不被回复浮窗规则压扁 */
          .qp-overlay-quote .qp-quote {
            height: auto !important;
            max-height: 100% !important;
            overflow: auto !important;
            display: block !important;
          }
          /* 浮窗内预览框及其子层级全部占满宽度 */
          .qp-body .qp-content-wrap .h-preview-box,
          .qp-body .qp-content-wrap .h-preview-box .h-threads-item,
          .qp-body .qp-content-wrap .h-preview-box .h-threads-item-replies,
          .qp-body .qp-content-wrap .h-preview-box .h-threads-item-reply,
          .qp-body .qp-content-wrap .h-preview-box .h-threads-item-reply-main {
              width: 100% !important;
              max-width: none !important;
              margin: 0 !important;
              box-sizing: border-box;
              display: block;
              background: var(--xdex-qp-preview-bg);
          }
          /* 浮窗内:仅在非 h-active 状态下限制缩略图宽度 */
          .qp-body .qp-content-wrap .h-preview-box .h-threads-img-box:not(.h-active) .h-threads-img {
            max-width: 33% !important;
            height: auto !important;
          }
          /* 表单内:仅在非 h-active 状态下限制缩略图宽度 */
          #h-post-form .h-preview-box .h-threads-img-box:not(.h-active) .h-threads-img {
            max-width: 50% !important;
            height: auto !important;
          }
          /* 扩展坞(与 ensureRightSidebarShellStyle 保持一致的最新版本,兜底双份冗余) */
          #h-tool { display: none !important; }
          .hld__docker { position: fixed; height: 80px; width: 30px; bottom: 180px; right: 0; transition: width .12s ease, height .12s ease, bottom .12s ease; z-index: 9998; }
          .hld__docker:hover,
          .hld__docker.is-hover { width: 150px; height: 300px; bottom: 75px; }
          .hld__docker:has(.hld__docker-sidebar:hover) { width: 150px; height: 300px; bottom: 75px; }
          .hld__docker.xdex-docker-boot,
          .hld__docker.xdex-docker-boot:hover,
          .hld__docker.xdex-docker-boot.is-hover,
          .hld__docker.xdex-docker-boot:has(.hld__docker-sidebar:hover) { transition: none; }
          .hld__docker-sidebar { background: #fff; position: fixed; height: 50px; width: 20px; bottom: 195px; right: 0; display: flex; justify-content: center; align-items: center; border: 1px solid #CCC; box-shadow: 0 0 1px #333; border-right: none; border-radius: 5px 0 0 5px; }
          .hld__docker-btns { position: absolute; top: 0; left: 50px; bottom: 0; right: 50px; display: flex; justify-content: center; align-items: center; flex-direction: column; }
          .hld__docker .hld__docker-btns>div { opacity: 0; flex-shrink: 0; pointer-events: none; transition: opacity .06s ease; }
          .hld__docker:hover .hld__docker-btns>div,
          .hld__docker.is-hover .hld__docker-btns>div,
          .hld__docker:has(.hld__docker-sidebar:hover) .hld__docker-btns>div { opacity: 1; pointer-events: auto; }
          .hld__docker.xdex-docker-boot .hld__docker-btns>div { transition: none; }
          .hld__docker-btns>div { background: #fff; border: 1px solid #CCC; box-shadow: 0 0 1px #444; width: 50px; height: 50px; border-radius: 50%; margin: 7px 0; cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 20px; font-weight: bold; color: #333; transition: background .12s ease, transform .12s ease, opacity .06s ease; }
          .hld__docker-btns>div:hover { background: #f0f0f0; transform: scale(1.1); }
          .hld__docker-btns>div svg { display: block; }
          .hld__docker-btns>div svg path { fill: currentColor; }
          .hld__docker-btns>div[data-type="PREV-THREAD"],
          .hld__docker-btns>div[data-type="NEXT-THREAD"],
          .hld__docker-btns>div[data-type="TOP"],
          .hld__docker-btns>div[data-type="BOTTOM"] { width: 41px; height: 41px; font-size: 16px; }
          /* 固定模式:dock 收窄贴右、三按钮常显、隐藏左侧把手(不依赖 hover/:has,回退机制不受影响) */
          .hld__docker.xdex-dock-fixed { width: 60px; height: 300px; bottom: 75px; }
          .hld__docker.xdex-dock-fixed:hover,
          .hld__docker.xdex-dock-fixed.is-hover,
          .hld__docker.xdex-dock-fixed:has(.hld__docker-sidebar:hover) { width: 60px; height: 300px; bottom: 75px; }
          .hld__docker.xdex-dock-fixed .hld__docker-sidebar { display: none; }
          .hld__docker.xdex-dock-fixed .hld__docker-btns { left: 0; right: 0; }
          .hld__docker.xdex-dock-fixed .hld__docker-btns>div { opacity: 1; pointer-events: auto; }
          .hld__docker.xdex-dock-boot.xdex-dock-fixed { transition: none; }
        `;
      if (!style.parentNode) {
        (document.head || document.documentElement).appendChild(style);
      }
    }
    ensureDarkReaderThemeSync();
    return !!document.getElementById('qp-style');
  }
  function ensureRightSidebarReplyController() {
    if (window.__xdexRightSidebarReplyController) return window.__xdexRightSidebarReplyController;
    // 悬浮窗引用
    let overlay;
    function ensureOverlay() {
      if (!overlay) {
          overlay = document.createElement('div');
          overlay.className = 'qp-overlay';
          overlay.innerHTML = `
              <div class="qp-stack">
                  <div class="qp-quote">
                      <div class="qp-drag-edge top"></div>
                      <div class="qp-drag-edge bottom"></div>
                      <div class="qp-drag-edge left"></div>
                      <div class="qp-drag-edge right"></div>
                      <div class="qp-resize-corner nw" data-dir="nw"></div>
                      <div class="qp-resize-corner ne" data-dir="ne"></div>
              <div class="qp-reset-btn xdex-icon-btn" title="回复浮窗归位"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path style="fill:none" d="M18 18A8.5 8.5 0 1 1 18.5 6.5"/><path style="fill:none" d="M18.5 6.5l-1.3 3.6"/><path style="fill:none" d="M19.2 10.6L18.5 6.5l-3.2 2.7"/></svg></div>
                      <div class="qp-resize-corner se" data-dir="se"></div>
                      <div class="qp-body"></div>
                  </div>
              </div>
              <div class="qp-reset-btn"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path style="fill:none" d="M18 18A8.5 8.5 0 1 1 18.5 6.5"/><path style="fill:none" d="M18.5 6.5l-1.3 3.6"/><path style="fill:none" d="M19.2 10.6L18.5 6.5l-3.2 2.7"/></svg></div>
          `;
          document.body.appendChild(overlay);
          // 点击遮罩关闭(点内容不关闭,且允许事件冒泡到 document 以触发引用弹窗)
          overlay.addEventListener('click', function (e) {
            // 冒泡途中若有控件替换了自身内容(如 .js-extra 切换图标),e.target 会脱离文档,
            // closest('.qp-quote') 随即失效、被误判成「点了遮罩」;改用派发时定格的 composedPath 判定
            const evPath = typeof e.composedPath === 'function' ? e.composedPath() : null;
            const inQuote = evPath
                ? evPath.some(el => el && el.classList && el.classList.contains('qp-quote'))
                : !!(e.target && e.target.closest && e.target.closest('.qp-quote'));
            if (inQuote) {
                return; // 点击在内容区:不关闭,也不阻止冒泡
            }
            // 如果正在调整 textarea 大小,不关闭浮窗
            if (overlay.__isResizing) {
                return;
            }
            closeOverlay();
          });
          // ESC 关闭(优先关闭颜文字选择框)
          document.addEventListener('keydown', e => {
            if (e.key !== 'Escape') return;
            // 检查是否存在打开的颜文字面板
            const openKaomojiPanel = document.querySelector('.kaomoji-panel[style*="display: grid"]');
            if (openKaomojiPanel) {
                // 触发颜文字函数内的关闭逻辑(模拟点击空白或ESC)
                openKaomojiPanel.style.display = 'none';
                // 若有全局绑定的 hidePanel 函数(例如触发按钮附近定义的),触发它
                const hideEvent = new CustomEvent('kaomoji:hide');
                document.dispatchEvent(hideEvent);
                //openKaomojiPanel.style.display = 'none';
                // ★ 新增:关闭颜文字面板后聚焦到浮窗内的 textarea
                const ta = overlay?.querySelector('textarea[name="content"]');
                if (ta) {
                  ta.focus();
                }
                return; // 阻止继续关闭浮窗
            }
            // 若没有颜文字面板打开,则关闭回复浮窗
            closeOverlay();
          });
          // 归位按钮事件:恢复默认居中尺寸(并重新贴合内容)
          const resetBtn = overlay.querySelector('.qp-reset-btn');
          resetBtn.addEventListener('click', (e) => {
              e.stopPropagation(); // 阻止事件冒泡,避免触发 overlay 的关闭逻辑
              overlay.__savedPanelRect = null;
              overlay.__userSizedPanel = false;
              applyReplyPanelLayout(overlay, null);
              scheduleReplyPanelAutoFit(overlay);
          });
      }
      return overlay;
    }
    function closeOverlay() {
      if (!overlay) return;
      // 清理 ResizeObserver(旧链路兜底)
      if (overlay.__resizeObserver) {
        overlay.__resizeObserver.disconnect();
        overlay.__resizeObserver = null;
      }
      // 清理内容贴合观察
      if (overlay.__autoFitRO) {
        try { overlay.__autoFitRO.disconnect(); } catch (_) {}
        overlay.__autoFitRO = null;
      }
      if (overlay.__autoFitRaf) {
        cancelAnimationFrame(overlay.__autoFitRaf);
        overlay.__autoFitRaf = 0;
      }
      // 页面内记住整窗位置/尺寸(不写 GM_setValue)
      // 仅手动四角拉伸过的尺寸需要保留;内容自然撑高的高度关闭后丢弃,下次快速回空高
      if (overlay.__userSizedPanel) {
        saveReplyPanelRect(overlay);
      } else {
        const stack = overlay.querySelector('.qp-stack');
        if (stack) {
          const rect = stack.getBoundingClientRect();
          // 只记位置/宽度,高度不记,避免被预览撑开的高度“粘住”
          overlay.__savedPanelRect = {
            winW: Math.round(overlay.__panelWinW || window.innerWidth),   // 布局时窗口宽度: 打开期间 resize 后关闭也能在下次重开时重新居中
            width: Math.round(rect.width),
            left: Math.round(rect.left),
            top: Math.round(rect.top),
            userSized: false
            // height 故意不写:open 时走默认空高 + 快速 autofit
          };
        } else {
          overlay.__savedPanelRect = null;
        }
        overlay.__userSizedPanel = false;
      }
      overlay.style.display = 'none';
      // 隐藏归位按钮
      const resetBtn = overlay.querySelector('.qp-reset-btn');
      if (resetBtn) resetBtn.style.display = 'none';
      // 清理窗口 resize 监听(旧链路兜底)
      if (overlay.__windowResizeHandler) {
        window.removeEventListener('resize', overlay.__windowResizeHandler);
        overlay.__windowResizeHandler = null;
      }
      if (overlay.__formEl && overlay.__formEl.__placeholder) {
        overlay.__formEl.__placeholder.parentNode.insertBefore(overlay.__formEl, overlay.__formEl.__placeholder);
        // 移回后清除 textarea 的宽度样式,让它恢复原始状态
        const taInForm = overlay.__formEl.querySelector('textarea[name="content"]');
        if (taInForm) {
          taInForm.style.removeProperty('width');
          taInForm.style.removeProperty('height');
          taInForm.style.removeProperty('min-width');
          taInForm.style.removeProperty('max-width');
        }
          if (overlay.__formEl.__wasCollapsed) {
            const formEl = overlay.__formEl;
            const $form = $(formEl);
            const hint = formEl.action.includes('doReplyThread') ? '『回复』' : '『发串』';
            // 优先还原 early collapse 留下的折叠按钮;否则走通用 collapse
            const toggleEl = formEl.__collapseToggleEl
              || ($form.prev('.xdex-placeholder.xdex-generic-toggle')[0] || null);
            if (toggleEl && toggleEl.parentNode) {
              formEl.classList.add('xdex-generic-collapsed');
              $form.hide().data('xdex-collapsed', true);
              toggleEl.style.display = '';
              toggleEl.innerHTML = `${hint}(点击展开)`;
            } else if ($form.prev('.xdex-placeholder').length) {
                $form.hide().data('xdex-collapsed', true);
            } else if (typeof Utils !== 'undefined' && typeof Utils.collapse === 'function') {
                Utils.collapse($form, hint);
            }
            formEl.__wasCollapsed = false;
            formEl.__collapseToggleEl = null;
        }
      }
      if (overlay.__previewEl && overlay.__previewEl.__placeholder) {
        overlay.__previewEl.__placeholder.parentNode.insertBefore(overlay.__previewEl, overlay.__previewEl.__placeholder);
      }
    }
    // 回复浮窗:整窗拖拽 + 四角缩放(页面内记忆,不写 GM_setValue)
    const REPLY_PANEL_MIN_WIDTH = 400;
    const REPLY_PANEL_MIN_HEIGHT = 280;
    const REPLY_PANEL_MAX_VH = 0.8;
    // 默认垂直位置:0.5=正中,越小越靠上。0.28 ≈ 偏上约 1/3
    const REPLY_PANEL_DEFAULT_TOP_RATIO = 0.28;
    function getReplyPanelMaxHeight() {
      return Math.max(REPLY_PANEL_MIN_HEIGHT, Math.min(window.innerHeight * REPLY_PANEL_MAX_VH, window.innerHeight - 40));
    }
    function getReplyPanelDefaultTop(height) {
      const h = Math.max(REPLY_PANEL_MIN_HEIGHT, Number(height) || REPLY_PANEL_MIN_HEIGHT);
      // 比垂直居中更靠上,同时保证不贴顶、底部也留一点余量
      const raw = Math.round((window.innerHeight - h) * REPLY_PANEL_DEFAULT_TOP_RATIO);
      return Math.max(8, Math.min(raw, window.innerHeight - h - 8));
    }
    function getReplyPanelDefaults() {
      const width = Math.max(REPLY_PANEL_MIN_WIDTH, Math.min(window.innerWidth * 0.9, 820));
      // 默认高度不再固定 80vh,只给一个贴合内容的保守初值;open 后会 auto-fit
      // 0.45 仍然偏高,会在表单/预览间留空;用更贴内容的初值
      const height = Math.max(REPLY_PANEL_MIN_HEIGHT, Math.min(Math.round(window.innerHeight * 0.32), getReplyPanelMaxHeight()));
      const left = Math.max(0, Math.round((window.innerWidth - width) / 2));
      const top = getReplyPanelDefaultTop(height);
      return { width, height, left, top };
    }
    function measureReplyPanelContentHeight(ov) {
      const quote = ov && ov.querySelector('.qp-quote');
      const wrap = ov && ov.querySelector('.qp-content-wrap');
      if (!quote || !wrap) return REPLY_PANEL_MIN_HEIGHT;
      const cs = window.getComputedStyle(quote);
      const padY = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0)
        + (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.borderBottomWidth) || 0);
      const form = wrap.querySelector('form');
      const preview = wrap.querySelector('.h-preview-box');
      const gap = 10; // 与 .qp-content-wrap 的 gap 对齐
      let contentH = 0;
      if (form) contentH += form.offsetHeight || form.getBoundingClientRect().height || 0;
      if (preview) {
        // 预览在 flex/overflow 约束下 offsetHeight 可能是被裁后的值,临时放开量真实内容高
        const prev = {
          maxHeight: preview.style.maxHeight,
          height: preview.style.height,
          overflow: preview.style.overflow,
          flex: preview.style.flex
        };
        preview.style.maxHeight = 'none';
        preview.style.height = 'auto';
        preview.style.overflow = 'visible';
        preview.style.flex = 'none';
        contentH += preview.scrollHeight || preview.offsetHeight || 0;
        preview.style.maxHeight = prev.maxHeight;
        preview.style.height = prev.height;
        preview.style.overflow = prev.overflow;
        preview.style.flex = prev.flex;
      } else {
        // 无预览时仍量 wrap 自身
        contentH = Math.max(contentH, wrap.scrollHeight || wrap.offsetHeight || 0);
      }
      if (form && preview) contentH += gap;
      return Math.max(REPLY_PANEL_MIN_HEIGHT, Math.ceil(contentH + padY + 2));
    }
    function fitReplyPanelToContent(ov, opts) {
      if (!ov || ov.__userSizedPanel || ov.__autoFitting) return;
      const stack = ov.querySelector('.qp-stack');
      if (!stack || ov.style.display === 'none') return;
      const options = opts || {};
      // 量高会临时改 preview 样式,期间屏蔽 RO,避免“量高→RO→再 fit”反馈环
      ov.__autoFitting = true;
      try {
        const maxH = getReplyPanelMaxHeight();
        const needed = Math.min(measureReplyPanelContentHeight(ov), maxH);
        const rect = stack.getBoundingClientRect();
        // 高度几乎不变时不写 style;no-op 时不要开静默窗,否则会吞掉紧随其后的预览增高
        const heightDelta = Math.abs((rect.height || 0) - needed);
        // 滞回判定: 进入/退出滚动态需跨越阈值带, 避免内容高度临界抖动导致滚动条反复出现/消失、横向挤压布局
        const wasScroll = stack.classList.contains('qp-content-scroll');
        const atCap = wasScroll ? (needed > maxH - 24) : (needed >= maxH - 1);
        stack.classList.toggle('qp-content-scroll', atCap);
        if (heightDelta < 2) {
          if (options.save !== false) {
            ov.__savedPanelRect = {
              winW: Math.round(ov.__panelWinW || window.innerWidth),
              width: Math.round(rect.width),
              height: Math.round(rect.height),
              left: Math.round(rect.left),
              top: Math.round(rect.top),
              userSized: false
            };
          }
          ov.__skipAutoFitQuiet = true;
          return;
        }
        let left = rect.left;
        let top = rect.top;
        let width = rect.width || getReplyPanelDefaults().width;
        // 未手动缩放时:优先向下扩展(顶边不动),避免“预览往上挤表单”
        if (!Number.isFinite(left)) left = Math.max(0, Math.round((window.innerWidth - width) / 2));
        if (!Number.isFinite(top) || top < 0) top = getReplyPanelDefaultTop(needed);
        // 只有底部真的装不下时才上移;否则保持 top,让高度向下长
        if (top + needed > window.innerHeight - 8) {
          top = Math.max(8, window.innerHeight - needed - 8);
        }
        // 宽度无效时回退默认
        if (!width || width < REPLY_PANEL_MIN_WIDTH) width = getReplyPanelDefaults().width;
        applyReplyPanelLayout(ov, {
          width,
          height: needed,
          left,
          top
        }, { preserveUserSized: true, fromAutoFit: true });
        if (options.save !== false) {
          // auto-fit 只记当前几何,不把 userSized 标真
          const r = stack.getBoundingClientRect();
          ov.__savedPanelRect = {
            winW: Math.round(ov.__panelWinW || window.innerWidth),
            width: Math.round(r.width),
            height: Math.round(r.height),
            left: Math.round(r.left),
            top: Math.round(r.top),
            userSized: false
          };
        }
      } finally {
        ov.__autoFitting = false;
        // 仅在真正改了尺寸时短时静默;no-op 贴高不能静默,否则挡住预览写完后的二次贴高
        if (ov.__skipAutoFitQuiet) {
          ov.__skipAutoFitQuiet = false;
        } else {
          ov.__autoFitQuietUntil = performance.now() + 48;
        }
      }
    }
    function scheduleReplyPanelAutoFit(ov) {
      if (!ov || ov.__userSizedPanel || ov.__autoFitting) return;
      if (ov.__autoFitQuietUntil && performance.now() < ov.__autoFitQuietUntil) return;
      if (ov.__autoFitRaf) cancelAnimationFrame(ov.__autoFitRaf);
      // 双 rAF:等布局/预览重建完成后再量高,合并同一帧内的多次 input/RO
      ov.__autoFitRaf = requestAnimationFrame(() => {
        ov.__autoFitRaf = requestAnimationFrame(() => {
          ov.__autoFitRaf = 0;
          if (ov.__userSizedPanel || ov.__autoFitting) return;
          if (ov.__autoFitQuietUntil && performance.now() < ov.__autoFitQuietUntil) return;
          fitReplyPanelToContent(ov);
        });
      });
    }
    function ensureReplyPanelAutoFitObserver(ov) {
      if (!ov) return;
      if (ov.__autoFitRO) {
        try { ov.__autoFitRO.disconnect(); } catch (_) {}
        ov.__autoFitRO = null;
      }
      const wrap = ov.querySelector('.qp-content-wrap');
      const ta = ov.querySelector('textarea[name="content"]');
      if (!wrap || typeof ResizeObserver === 'undefined') return;
      const onMaybeAutoFit = () => {
        if (ov.__userSizedPanel || ov.__autoFitting) return;
        if (ov.__autoFitQuietUntil && performance.now() < ov.__autoFitQuietUntil) return;
        scheduleReplyPanelAutoFit(ov);
      };
      const ro = new ResizeObserver(onMaybeAutoFit);
      // 只观察 wrap/preview。不要监听 textarea input:
      // input 时预览尚未更新,会先 no-op 贴高并制造滚动条闪动窗口。
      ro.observe(wrap);
      // 预览内容变化(首次出字/粘贴图/引用展开)也跟
      const preview = ov.querySelector('.h-preview-box');
      if (preview) ro.observe(preview);
      ov.__autoFitRO = ro;
      // 暴露给 renderContent:预览写完后立刻排队贴高
      window.__xdexScheduleReplyPanelFit = () => scheduleReplyPanelAutoFit(ov);
    }
    function applyReplyPanelLayout(ov, rect, opts) {
      const stack = ov && ov.querySelector('.qp-stack');
      const quote = ov && ov.querySelector('.qp-quote');
      if (!stack) return;
      const options = opts || {};
      const base = getReplyPanelDefaults();
      const r = rect && typeof rect === 'object' ? rect : base;
      let width = Math.max(REPLY_PANEL_MIN_WIDTH, Number(r.width) || base.width);
      let height = Math.max(REPLY_PANEL_MIN_HEIGHT, Number(r.height) || base.height);
      width = Math.min(width, window.innerWidth);
      // 未手动缩放时,高度上限 80vh;手动缩放后允许到视口高
      const maxH = ov && ov.__userSizedPanel ? window.innerHeight : getReplyPanelMaxHeight();
      height = Math.min(height, maxH);
      let left = Number.isFinite(Number(r.left)) ? Number(r.left) : base.left;
      let top = Number.isFinite(Number(r.top)) ? Number(r.top) : base.top;
      // 允许略微拖出视口,但至少保留 50px 可抓取
      left = Math.min(Math.max(left, -width + 50), window.innerWidth - 50);
      top = Math.min(Math.max(top, -height + 50), window.innerHeight - 50);
      stack.style.position = 'fixed';
      stack.style.transform = 'none';
      stack.style.width = width + 'px';
      stack.style.height = height + 'px';
      stack.style.left = left + 'px';
      stack.style.top = top + 'px';
      stack.style.maxWidth = 'none';
      stack.style.minWidth = REPLY_PANEL_MIN_WIDTH + 'px';
      stack.style.maxHeight = maxH + 'px';
      // 用户手动尺寸时允许内部 flex 填满;自动贴合时保持内容高度,避免预览被拉出空隙
      if (ov.__userSizedPanel) stack.classList.add('qp-user-sized');
      else stack.classList.remove('qp-user-sized');
      if (quote) {
        quote.style.top = '0px';
        quote.style.left = '0px';
        quote.style.width = '100%';
        quote.style.height = '100%';
      }
      if (!options.preserveUserSized && r && r.userSized) {
        ov.__userSizedPanel = true;
        stack.classList.add('qp-user-sized');
      }
    }
    function saveReplyPanelRect(ov) {
      const stack = ov && ov.querySelector('.qp-stack');
      if (!stack || !ov) return;
      const rect = stack.getBoundingClientRect();
      ov.__savedPanelRect = {
        winW: Math.round(window.innerWidth),
        width: Math.round(rect.width),
        height: Math.round(rect.height),
        left: Math.round(rect.left),
        top: Math.round(rect.top),
        userSized: !!ov.__userSizedPanel
      };
    }

    function bindReplyPanelFrame(ov) {
      if (!ov || ov.__replyPanelFrameBound) return;
      const stack = ov.querySelector('.qp-stack');
      const quote = ov.querySelector('.qp-quote');
      if (!stack || !quote) return;
      ov.__replyPanelFrameBound = true;

      const onEdgeDown = (e) => {
        if (e.button !== 0) return;
        e.preventDefault();
        e.stopPropagation();
        const startX = e.clientX;
        const startY = e.clientY;
        const startRect = stack.getBoundingClientRect();
        quote.classList.add('is-dragging');
        ov.__isResizing = true; // 避免拖拽结束瞬间点到遮罩关闭

        const onMove = (ev) => {
          ev.preventDefault();
          const dx = ev.clientX - startX;
          const dy = ev.clientY - startY;
          let left = startRect.left + dx;
          let top = startRect.top + dy;
          left = Math.min(Math.max(left, -startRect.width + 50), window.innerWidth - 50);
          top = Math.min(Math.max(top, -startRect.height + 50), window.innerHeight - 50);
          stack.style.transform = 'none';
          stack.style.left = left + 'px';
          stack.style.top = top + 'px';
        };
        const onUp = () => {
          quote.classList.remove('is-dragging');
          setTimeout(() => { ov.__isResizing = false; }, 100);
          saveReplyPanelRect(ov);
          window.removeEventListener('mousemove', onMove);
          window.removeEventListener('mouseup', onUp);
        };
        window.addEventListener('mousemove', onMove);
        window.addEventListener('mouseup', onUp);
      };
      const onCornerDown = (e) => {
        if (e.button !== 0) return;
        const dir = e.currentTarget.getAttribute('data-dir') || '';
        if (!dir) return;
        e.preventDefault();
        e.stopPropagation();
        const startX = e.clientX;
        const startY = e.clientY;
        const startRect = stack.getBoundingClientRect();
        // 对角锚定:拖动某角时,对侧角位置保持不动
        const anchorRight = startRect.right;
        const anchorBottom = startRect.bottom;
        quote.classList.add('is-resizing');
        ov.__isResizing = true;
        // 用户一旦四角拉伸,就关闭内容自动贴合高度
        ov.__userSizedPanel = true;
        const onMove = (ev) => {
          ev.preventDefault();
          const dx = ev.clientX - startX;
          const dy = ev.clientY - startY;
          let left = startRect.left;
          let top = startRect.top;
          let width = startRect.width;
          let height = startRect.height;

          if (dir.indexOf('e') !== -1) {
            width = startRect.width + dx;
          }
          if (dir.indexOf('s') !== -1) {
            height = startRect.height + dy;
          }
          if (dir.indexOf('w') !== -1) {
            width = startRect.width - dx;
            left = anchorRight - width;
          }
          if (dir.indexOf('n') !== -1) {
            height = startRect.height - dy;
            top = anchorBottom - height;
          }

          if (width < REPLY_PANEL_MIN_WIDTH) {
            if (dir.indexOf('w') !== -1) left = anchorRight - REPLY_PANEL_MIN_WIDTH;
            width = REPLY_PANEL_MIN_WIDTH;
          }
          if (height < REPLY_PANEL_MIN_HEIGHT) {
            if (dir.indexOf('n') !== -1) top = anchorBottom - REPLY_PANEL_MIN_HEIGHT;
            height = REPLY_PANEL_MIN_HEIGHT;
          }
          if (width > window.innerWidth) width = window.innerWidth;
          if (height > window.innerHeight) height = window.innerHeight;

          stack.style.transform = 'none';
          stack.style.left = left + 'px';
          stack.style.top = top + 'px';
          stack.style.width = width + 'px';
          stack.style.height = height + 'px';
          stack.style.maxHeight = window.innerHeight + 'px';
        };
        const onUp = () => {
          quote.classList.remove('is-resizing');
          setTimeout(() => { ov.__isResizing = false; }, 100);
          saveReplyPanelRect(ov);
          window.removeEventListener('mousemove', onMove);
          window.removeEventListener('mouseup', onUp);
        };
        window.addEventListener('mousemove', onMove);
        window.addEventListener('mouseup', onUp);
      };
      quote.querySelectorAll('.qp-drag-edge').forEach((el) => {
        el.addEventListener('mousedown', onEdgeDown);
      });
      quote.querySelectorAll('.qp-resize-corner').forEach((el) => {
        el.addEventListener('mousedown', onCornerDown);
      });
    }
    // REPLY 按钮
    function open() {
      let formEl = document.querySelector('form[action="/Home/Forum/doReplyThread.html"]');
      let previewEl = document.querySelector('.h-preview-box');
      // 如果串内没找到表单,尝试板块页发串表单
      if (!formEl) {
          const postForm = document.querySelector('#h-post-form form[action="/Home/Forum/doPostThread.html"]');
          if (postForm) {
              formEl = postForm;
              previewEl = document.querySelector('#h-post-form .h-preview-box');
          }
      }
      if (!formEl) {
          toast && toast('未找到回复/发串表单');
          return;
      }
      // early open 也必须先注入浮窗样式/主题,避免表单先以无样式状态闪一下
      // batch2 可能已把 qp-style 延后到 idle;此处是悬停优化后的同步兜底
      ensureReplyOverlayStyle();
      try { ensureDarkReaderThemeObserver(); } catch (e) {}
      const ov = ensureOverlay();
      const body = ov.querySelector('.qp-body');
      body.innerHTML = '';
      // 占位符
      if (!formEl.__placeholder) {
          const ph1 = document.createElement('div');
          ph1.style.display = 'none';
          formEl.parentNode.insertBefore(ph1, formEl);
          formEl.__placeholder = ph1;
      }
      if (previewEl && !previewEl.__placeholder) {
          const ph2 = document.createElement('div');
          ph2.style.display = 'none';
          previewEl.parentNode.insertBefore(ph2, previewEl);
          previewEl.__placeholder = ph2;
      }
      // 折叠表单先只记状态,不要在原位 show()——否则 early open 时表单会先裸奔到页面上。
      // 真正展开放到 body.appendChild(wrap) 之后、overlay 显示之前。
      // 同时处理 Utils.collapse 留下的可见折叠按钮,避免它留在原位/被误搬进浮窗。
      const $formEl = $(formEl);
      const wasCollapsedForOpen = !!$formEl.data('xdex-collapsed') || formEl.classList.contains('xdex-generic-collapsed');
      if (wasCollapsedForOpen) {
          formEl.__wasCollapsed = true; // ★ 记录原本是折叠的
          let collapseToggleEl = null;
          const prev = formEl.previousElementSibling;
          if (prev && prev.classList.contains('xdex-placeholder') && prev.classList.contains('xdex-generic-toggle')) {
            collapseToggleEl = prev;
          } else {
            const $ph = $formEl.prev('.xdex-placeholder.xdex-generic-toggle');
            if ($ph.length) collapseToggleEl = $ph[0];
          }
          if (collapseToggleEl) {
            collapseToggleEl.style.display = 'none';
            formEl.__collapseToggleEl = collapseToggleEl;
          }
          $formEl.removeData('xdex-collapsed');
          formEl.classList.remove('xdex-generic-collapsed');
      }
      // === 新增:给表单打标记,避免被板块页展开逻辑 / early collapse 误处理 ===
      formEl.classList.add('qp-reply-form');
      // 包装容器,防止 UI 松散
      const wrap = document.createElement('div');
      wrap.className = 'qp-content-wrap';
      // 表单是必需的
      wrap.appendChild(formEl);
      // 预览框是可选的
      if (previewEl) {
        wrap.appendChild(previewEl);
        // 浮窗内:强制预览框及其 replies 拉满宽度,清理站点样式的限宽/居中
        if (previewEl) {
          // 预览框本身满宽
          previewEl.style.width = '100%';
          previewEl.style.maxWidth = 'none';
          previewEl.style.margin = '0';
          // replies 拉满
          const replies = previewEl.querySelectorAll('.h-threads-item-replies');
          replies.forEach(el => {
            el.style.width = '100%';
            el.style.maxWidth = 'none';
            el.style.margin = '0';
            el.style.display = 'block';
            el.style.boxSizing = 'border-box';
          });
          // 如有外层 item/容器被限宽,一并放开
          const items = previewEl.querySelectorAll('.h-threads-item');
          items.forEach(el => {
            el.style.width = '100%';
            el.style.maxWidth = 'none';
            el.style.margin = '0';
            el.style.boxSizing = 'border-box';
          });
        }
      }
      body.appendChild(wrap);
      // 表单已进浮窗后再展开,避免原位闪现/拆碎漂在左上角;
      // 同时确保 early collapse 的折叠按钮不会出现在浮窗里。
      if (wasCollapsedForOpen) {
        $(formEl).show();
        formEl.classList.remove('xdex-generic-collapsed');
        if (formEl.__collapseToggleEl && formEl.__collapseToggleEl.parentNode) {
          // 折叠按钮留在原位隐藏即可;不要带进浮窗
          formEl.__collapseToggleEl.style.display = 'none';
        }
        // 只清理整表单『回复/发串』折叠按钮;不要删 form 内的「可选项」按钮
        if (formEl.__collapseToggleEl && wrap.contains(formEl.__collapseToggleEl)) {
          formEl.__collapseToggleEl.remove();
          formEl.__collapseToggleEl = null;
        }
      }
      // early open:ready/batch2 可能还没跑 enhancePostFormLayout,
      // 导致浮窗内没有“可选项”(标题/名称/E-mail)折叠按钮、送出按钮也还在标题行。
      // 这里对当前 form 同步补做一次(函数内部已幂等)。
      try {
        if (typeof enhancePostFormLayout === 'function') enhancePostFormLayout(formEl);
      } catch (e) {}
      // early open:若预览框尚未创建,立刻按当前 form 补一个 skeleton,
      // 避免用户先看到无预览表单,再等 enhanceIsland/batch2。
      if (!previewEl || !previewEl.isConnected || !wrap.contains(previewEl)) {
        try {
          if (typeof tryInsertPreviewBoxEarly === 'function') tryInsertPreviewBoxEarly(formEl);
        } catch (e) {}
        // 优先取紧邻当前 form 的预览,再兜底全局
        previewEl = (formEl.nextElementSibling && formEl.nextElementSibling.classList?.contains('h-preview-box'))
          ? formEl.nextElementSibling
          : (wrap.querySelector('.h-preview-box')
            || document.querySelector('.qp-body .h-preview-box')
            || document.querySelector('.h-preview-box'));
        if (previewEl && !wrap.contains(previewEl)) {
          if (!previewEl.__placeholder) {
            const ph2 = document.createElement('div');
            ph2.style.display = 'none';
            if (previewEl.parentNode) previewEl.parentNode.insertBefore(ph2, previewEl);
            previewEl.__placeholder = ph2;
          }
          previewEl.style.width = '100%';
          previewEl.style.maxWidth = 'none';
          previewEl.style.margin = '0';
          wrap.appendChild(previewEl);
        }
      }
      // 确保粘贴图片预览已绑定(幂等),即使 enhanceIsland 尚未初始化,
      // 早期打开浮窗时粘贴图片也能被捕获并更新到当前活跃预览框。
      if (typeof bindPasteImagePreviewOnce === 'function') bindPasteImagePreviewOnce();
      // === 全面修复:搬运“滞后出现的预览框”进浮窗 ===
      // 若 open() 执行时页面还没有 .h-preview-box(enhanceIsland 尚未初始化),
      // 之后 enhanceIsland 会把新的预览框插到浮窗外(原位置)。如果此时浮窗里
      // 还没有预览框,就把浮窗外的那个搬进来,否则粘贴图片的预览会更新到
      // 用户看不见的框。短时重试几次,覆盖 batch2 稍晚创建的情况。
      const adoptStrayPreviewIntoPanel = () => {
        const bodyNow = ov.querySelector('.qp-body');
        if (!bodyNow) return false;
        const wrapNow = bodyNow.querySelector('.qp-content-wrap') || wrap;
        if (bodyNow.querySelector('.h-preview-box')) {
          // 浮窗内已有:仍补一次表单布局(幂等),防止只补了预览漏了可选项
          try { if (typeof enhancePostFormLayout === 'function') enhancePostFormLayout(formEl); } catch (e) {}
          return true;
        }
        const stray = document.querySelector('.h-preview-box');
        if (!stray || stray.closest('.qp-body')) return false; // 没有,或已在别的浮窗
        // 记录占位符(与 closeOverlay 的还原逻辑对应)
        if (!stray.__placeholder) {
          const ph = document.createElement('div');
          ph.style.display = 'none';
          if (stray.parentNode) stray.parentNode.insertBefore(ph, stray);
          stray.__placeholder = ph;
        }
        stray.style.width = '100%';
        stray.style.maxWidth = 'none';
        stray.style.margin = '0';
        // 放进同一个 content wrap,避免再叠一个 wrap 导致布局错乱
        wrapNow.appendChild(stray);
        ov.__previewEl = stray;
        if (typeof extendQuote === 'function') extendQuote(stray);
        if (typeof initContent === 'function') {
          try { initContent(stray); } catch (e) {}
        }
        // 搬运完成后,如果 file input 里已有图片(用户在 enhanceIsland 之前就粘贴了),
        // 立即补渲染到刚搬进浮窗的预览框,避免“图片已插入但预览不显示”。
        const movedInput = wrapNow.querySelector('input[type="file"][name="image"]')
          || document.querySelector('.qp-body input[type="file"][name="image"]');
        if (movedInput && movedInput.files && movedInput.files[0]) {
          updatePreviewImageFromFile(movedInput.files[0]);
        }
        // 滞后预览框搬入后:立刻贴合,避免用户先看到只有表单的矮窗再被撑高
        ensureReplyPanelAutoFitObserver(ov);
        if (!ov.__userSizedPanel) fitReplyPanelToContent(ov, { save: true });
        scheduleReplyPanelAutoFit(ov);
        return true;
      };
      // 立刻试一次 + 短时重试(enhanceIsland 在 batch2,可能比 open 晚几十~几百 ms)
      if (!adoptStrayPreviewIntoPanel()) {
        let tries = 0;
        const timer = setInterval(() => {
          tries += 1;
          if (adoptStrayPreviewIntoPanel() || tries >= 12) clearInterval(timer);
        }, 50);
      }
      // 浮窗内立即处理扩展引用,保证可点击引用弹窗
      if (typeof extendQuote === 'function') {
        extendQuote(previewEl || wrap);
      }
      if (typeof initContent === 'function') {
        try { initContent(previewEl || wrap); } catch (e) {}
      }
      // if (typeof autoHideRefView === 'function') {
      //   try { autoHideRefView(root); } catch (e) { try { autoHideRefView(); } catch (e) {} }
      // }
      // 保存引用
      ov.__formEl = formEl;
      ov.__previewEl = previewEl;
      // 显示 overlay 前再同步一次主题,确保 darkreader 配色首帧就位
      if (typeof syncQuotePopupTheme === 'function') syncQuotePopupTheme();
      // 恢复/应用整窗布局,并绑定四边移动 + 四角缩放
      // 若上次是用户四角拉伸过,继续尊重其尺寸;
      // 非手动尺寸:先隐藏测量真实内容高,再露出,避免“先空高再撑高”的跳变

      let layoutRect = null;
      if (ov.__savedPanelRect && ov.__savedPanelRect.userSized) {
        ov.__userSizedPanel = true;
        layoutRect = ov.__savedPanelRect;
      } else {
        ov.__userSizedPanel = false;
        const saved = ov.__savedPanelRect || null;
        const base = getReplyPanelDefaults();
        // 记录本次布局计算时的视口宽度(而非关闭时刻的实时宽度):
        // 打开期间改变窗口宽度后关闭, 下次打开应重新居中而不是沿用旧几何
        ov.__panelWinW = Math.round(window.innerWidth);
        // 窗口宽度已变化: 上次保存的水平几何(位置/宽度)不可信, 按当前视口重新居中;
        // 垂直位置沿用上次(fit 阶段有越界上移兜底)
        const viewportWidthChanged = saved && Number.isFinite(Number(saved.winW))
          && Math.abs(Number(saved.winW) - window.innerWidth) > 2;
        const useSavedW = saved && saved.width && !viewportWidthChanged;
        const useSavedLeft = saved && Number.isFinite(Number(saved.left)) && !viewportWidthChanged;
        const useSavedTop = saved && Number.isFinite(Number(saved.top));
        layoutRect = {
          width: useSavedW ? saved.width : base.width,
          height: base.height, // 仅作测量前的初始几何;真正露出前会 fit 成内容高
          left: useSavedLeft ? saved.left : base.left,
          top: useSavedTop ? saved.top : base.top,
          userSized: false
        };
      }
      // 先不可见地挂到布局树,量准高度后再显示,避免用户看到尺寸变化
      ov.style.visibility = 'hidden';
      ov.style.display = 'block';
      applyReplyPanelLayout(ov, layoutRect);
      bindReplyPanelFrame(ov);
      if (!ov.__userSizedPanel) {
        // 同步贴合:此时 form/preview 已在 DOM,且 display=block,可量到真实高度
        fitReplyPanelToContent(ov, { save: true });
      }
      // 强制一次 reflow,再露出最终尺寸
      void (ov.offsetHeight);
      ov.style.visibility = '';
      // 观察器只负责后续内容变化(输入/粘贴图/引用展开),不再承担“首开定高”
      ensureReplyPanelAutoFitObserver(ov);
      // 轻量跟一次:处理图片/字体等晚一帧才稳定的情况(首帧已是内容高,通常几乎无跳变)
      scheduleReplyPanelAutoFit(ov);
      // 显示归位按钮
      const resetBtn = ov.querySelector('.qp-reset-btn');
      if (resetBtn) {
        resetBtn.style.display = 'flex';
        resetBtn.style.alignItems = 'center';
        resetBtn.style.justifyContent = 'center';
      }
      const ta = ov.querySelector('textarea[name="content"]');

      if (ta) {
        // 允许垂直缩放;宽度仍跟浮窗走
        ta.style.resize = 'vertical';
        ta.style.fontSize = '14px';
        ta.style.flex = 'none';
      }
      // 聚焦 textarea
      if (ta) {
          ta.focus();
          const val = ta.value;
          ta.value = '';
          ta.value = val;
          // 恢复引用追记保存的光标位置(关闭浮窗时点引用号,focus 会失败,
          // 所以在引用追记中把期望光标位置暂存在元素上,这里再恢复)
          if (ta.__pendingQuoteCursorPos != null) {
            ta.setSelectionRange(ta.__pendingQuoteCursorPos, ta.__pendingQuoteCursorPos);
            ta.__pendingQuoteCursorPos = null;
          }
      }
      // Ctrl+Enter 发送并在成功后关闭浮窗
      if (ta) {
          const form = ta.closest('form');
          if (form && !ta.__qpCtrlEnterBound) {
              ta.__qpCtrlEnterBound = true;
              // 保留:提交成功后关闭浮窗,并清掉发送锁/超时器
              document.addEventListener('replySuccess', () => {
                  if (form.__submitLockTimer) {
                    clearTimeout(form.__submitLockTimer);
                    form.__submitLockTimer = 0;
                  }
                  form.__submitting = false;
                  closeOverlay();
              });
              // 新增:调用抽取的绑定函数
              bindCtrlEnter(ta);
          }
      }
    }
    const controller = { open, close: closeOverlay };
    window.__xdexRightSidebarReplyController = controller;
    return controller;
  }

  function replaceRightSidebar() {
    const logRightSidebar = (stage, detail) => {
      console.log('[replaceRightSidebar]', stage, detail || {});
    };
    logRightSidebar('start', {
      原始工具栏数: $('#h-tool').length,
      既有拓展坞数: $('.hld__docker').length,
      既有样式: !!document.getElementById('qp-style'),
      readyState: document.readyState,
      href: location.href,
    });
    // 移除原始工具栏;early shell 可能已经处理过,这里兜底
    const hadDockerBefore = !!document.querySelector('.hld__docker');
    tryReplaceRightSidebarEarly();
    logRightSidebar('original-toolbar-removed', {
      remainingToolbarCount: $('#h-tool').length,
    });
    // 扩展坞 DOM:复用 early shell,避免加载时重复插入和重复重绘
    let dockerDom = $('.hld__docker').first();
    if (!dockerDom.length) {
      dockerDom = $(buildRightSidebarDockerHtml()).appendTo('body');
      if (dockerDom[0]) markRightSidebarDockerBootMotion(dockerDom[0]);
      bindRightSidebarShellButtons(dockerDom[0]);
    }
    const dockerEl = dockerDom[0];
    const earlyReady = !!(dockerEl && (dockerEl.dataset.xdexEarlyDocker === '1' || hadDockerBefore));
    const styleExisted = !!document.getElementById('qp-style');
    // 悬停展开只依赖 shell CSS;qp-style 仅服务 REPLY 浮窗。
    // batch2 若坞已在且样式未装:改为空闲预热,避免与首次悬停抢主线程。
    // 首次点 REPLY 时 open() 仍会 ensureReplyOverlayStyle 兜底。
    // 但 Dark Reader class 必须立刻同步:设置面板等 UI 依赖 :root.xdex-darkreader-active。
    try { ensureDarkReaderThemeSync(); } catch (e) {}
    if (!earlyReady && !styleExisted) {
      ensureReplyOverlayStyle();
      logRightSidebar('style-injected-sync', { 样式ID: 'qp-style', reason: 'docker-not-early' });
    } else if (!styleExisted) {
      scheduleReplyOverlayStylePrefetch();
      logRightSidebar('style-deferred-idle', { 样式ID: 'qp-style', reason: 'hover-path-priority' });
    } else {
      logRightSidebar('style-reused', { 样式ID: 'qp-style' });
    }
    replaceRightSidebar.__darkReaderObserver = window.__xdexDarkReaderThemeObserver || null;
    logRightSidebar('docker-ready', {
      拓展坞数: $('.hld__docker').length,
      按钮数: dockerDom.find('.hld__docker-btns>div').length,
      早期插入: dockerEl?.dataset.xdexEarlyDocker === '1',
      支持Has选择器: !!(window.CSS && CSS.supports && CSS.supports('selector(:has(*))')),
      样式策略: styleExisted ? 'reused' : (earlyReady ? 'idle-prefetch' : 'sync'),
    });
    // REPLY 按钮:early shell 已绑定;这里兜底补绑,完整浮窗控制器仍延迟到首次点击再初始化
    bindRightSidebarReplyButton(dockerEl);
    // 按设置应用「固定/隐藏」模式(early 已应用则幂等)
    try { applyDockDisplayMode(getDockDisplayMode()); } catch (e) {}
    // TOP/BOTTOM 在 early shell 阶段已绑定轻量滚动;这里不重复绑定,避免一次点击触发两套滚动。
  }

  /* --------------------------------------------------
   * tag 11. 拦截回复中间页
   * -------------------------------------------------- */
  // 回复提交文本工具:仅统一逐字符处理路径共用的 URL 范围保护原语
  function findUrlRanges(text) {
    const re = /(?:(?:https?|ftp):\/\/[^\s]+|www\.[^\s]+|[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s]*)?|>>(?:No\.)?\d+)/gi;
    const ranges = [];
    for (const m of text.matchAll(re)) {
      ranges.push({ start: m.index, end: m.index + m[0].length });
    }
    return ranges;
  }
  function inAnyRange(pos, ranges) {
    for (const r of ranges) {
      if (pos >= r.start && pos < r.end) return true;
    }
    return false;
  }
  function transformTextOutsideUrlRanges(text, transformChar) {
    const urlRanges = findUrlRanges(text);
    let out = '';
    for (let i = 0; i < text.length; i++) {
      const ch = text[i];
      out += inAnyRange(i, urlRanges) ? ch : transformChar(ch);
    }
    return out;
  }
  function fallbackInsertZWSP(text) {
    const hanRegex = /[\u4E00-\u9FFF]/;
    return transformTextOutsideUrlRanges(text, (ch) => hanRegex.test(ch) ? ch + '\u200B' : ch);
  }
  function insertZwspAfterHanAndAsciiLettersOutsideUrls(text) {
    const hanRegex = /[\u4E00-\u9FFF]/;
    const engRegex = /[A-Za-z]/;
    return transformTextOutsideUrlRanges(text, (ch) => (hanRegex.test(ch) || engRegex.test(ch)) ? ch + '\u200B' : ch);
  }

  // 回复提交图片/FormData 纯工具:不依赖表单提交状态
  function getImageExtension(fileName) {
    return (fileName || '').split('.').pop().toLowerCase();
  }
  function normalizeDetectedImageFormat(hex4, ext) {
    if (hex4 === '47494638') return 'gif';
    if (hex4.startsWith('ffd8ff')) return 'jpeg';
    if (hex4 === '89504e47') return 'png-container';
    if (hex4 === '52494646') return 'webp-container';
    if (hex4 === '424d') return 'bmp';
    if (ext === 'gif') return 'gif';
    if (ext === 'apng') return 'apng';
    if (ext === 'webp') return 'webp';
    if (ext === 'png') return 'png';
    if (ext === 'jpg' || ext === 'jpeg') return 'jpeg';
    if (ext === 'heic' || ext === 'heif' || ext === 'hif') return 'heic';
    return 'unknown';
  }
  function isHeifBrand(brand) {
    return /^(heic|heix|hevc|hevx|heim|heis|hevm|hevs|mif1|msf1|miaf)$/i.test(brand || '');
  }
  function detectHeifFromHeader(bytes) {
    if (!bytes || bytes.length < 12) return null;
    const type = String.fromCharCode(bytes[4], bytes[5], bytes[6], bytes[7]);
    if (type !== 'ftyp') return null;
    const brands = [];
    brands.push(String.fromCharCode(bytes[8], bytes[9], bytes[10], bytes[11]));
    for (let i = 16; i + 4 <= Math.min(bytes.length, 64); i += 4) {
      brands.push(String.fromCharCode(bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]));
    }
    return brands.some(isHeifBrand) ? 'heic' : null;
  }
  function getStaticImageOutputType(originalType) {
    if (originalType === 'image/webp') return 'image/webp';
    if (originalType === 'image/jpeg' || originalType === 'image/jpg') return 'image/jpeg';
    return 'image/png';
  }
  // 服务器图片体积上限(2048KB);预检与错误重试共用
  const SUBMIT_IMAGE_LIMIT_BYTES = 2048 * 1024;

  function cloneFormData(sourceFd) {
    const cloned = new FormData();
    for (const [key, value] of sourceFd.entries()) {
      cloned.append(key, value);
    }
    return cloned;
  }
  function cloneFormDataWithImage(sourceFd, image) {
    const retryFd = cloneFormData(sourceFd);
    retryFd.set('image', image);
    return retryFd;
  }

  function getCurrentSubmitContent(form, fallbackFd) {
    const textarea = form.querySelector('textarea[name="content"]');
    return textarea ? textarea.value : (fallbackFd.get('content') || '').toString();
  }

  function createContentRetryFormData(form, content) {
    const retryFd = new FormData(form);
    retryFd.set('content', content);
    // 预检压缩/转码过的图片优先复用,避免非法词重试时又上传原图
    const cached = form && form.__preparedImage;
    if (cached && cached.file) {
      const fileInput = form.querySelector('input[type="file"][name="image"]');
      const current = fileInput && fileInput.files && fileInput.files[0];
      if (current &&
          current.name === cached.name &&
          current.size === cached.size &&
          current.lastModified === cached.lastModified) {
        retryFd.set('image', cached.file);
      }
    }
    return retryFd;
  }

  function restoreOriginalSubmitContent(form) {
    const textarea = form.querySelector('textarea[name="content"]');
    if (textarea && form.__originalContent != null) {
      textarea.value = form.__originalContent;
    }
    return form.__originalContent;
  }

  function readBlobAsArrayBuffer(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = () => reject(reader.error || new Error('读取图片数据失败'));
      reader.readAsArrayBuffer(blob);
    });
  }

  // 通过文件头部魔数检测真实图片格式(静态 / 动画)
  async function detectImageFormat(file) {
    const ext = getImageExtension(file.name);
    let headerBuffer;
    try {
      // HEIF/ISOBMFF 需要读到 ftyp 兼容品牌区(至少 32~64 字节)
      headerBuffer = await readBlobAsArrayBuffer(file.slice(0, 64));
    } catch (_) {
      return normalizeDetectedImageFormat('', ext);
    }
    const arr = new Uint8Array(headerBuffer);
    const heifFormat = detectHeifFromHeader(arr);
    if (heifFormat) return heifFormat;
    const hex4 = Array.from(arr.subarray(0, 4), b => b.toString(16).padStart(2, '0')).join('').toLowerCase();
    const detectedFormat = normalizeDetectedImageFormat(hex4, ext);
    // PNG / APNG: 89 50 4e 47
    if (detectedFormat === 'png-container') {
      try {
        const fullBuffer = await readBlobAsArrayBuffer(file.slice(0, Math.min(file.size, 256 * 1024)));
        const fullArr = new Uint8Array(fullBuffer);
        let foundAcTl = false;
        for (let i = 8; i + 12 <= fullArr.length; ) {
          const len = (fullArr[i] << 24) | (fullArr[i + 1] << 16) | (fullArr[i + 2] << 8) | fullArr[i + 3];
          const type = String.fromCharCode(fullArr[i + 4], fullArr[i + 5], fullArr[i + 6], fullArr[i + 7]);
          if (type === 'acTL') { foundAcTl = true; break; }
          if (type === 'IDAT') break;
          i += 12 + len;
        }
        return foundAcTl ? 'apng' : 'png';
      } catch (_) {
        return 'png';
      }
    }
    // WebP: 52 49 46 46
    if (detectedFormat === 'webp-container') {
      try {
        const fullBuffer = await readBlobAsArrayBuffer(file.slice(0, Math.min(file.size, 4096)));
        const fullArr = new Uint8Array(fullBuffer);
        const str = String.fromCharCode.apply(null, Array.from(fullArr.subarray(0, Math.min(4096, fullArr.length))));
        return str.indexOf('ANIM') !== -1 ? 'animated-webp' : 'webp';
      } catch (_) {
        return 'webp';
      }
    }
    return detectedFormat;
  }

  function clearSuccessfulSubmitPreview() {
    const previewBox = document.querySelector('.h-preview-box');
    if (previewBox) {
      const cur = getCurrentCookie();
      const cookieText = cur ? cur.name : '--';
      // 先放一个占位 ID,等刷新完成后再更新
      previewBox.innerHTML = buildEnhanceIslandPreviewHtml(false);
      if (typeof enableHDImage === 'function') {
        enableHDImage(previewBox);
      }
    }
    if (typeof refreshCookies === 'function') {
      try {
        refreshCookies(() => {
          try {
            if (typeof updatePreviewCookieId === 'function') {
              updatePreviewCookieId();
            }
          } catch (err) {
            console.warn('[interceptReplyForm] updatePreviewCookieId after success failed', err);
          }
        }, false);
      } catch (err) {
        console.warn('[interceptReplyForm] refreshCookies after success failed', err);
        toast('回复已发送,但饼干刷新失败,请刷新页面', 0, { type: 'error' });
      }
    }
  }

  // ---- HEIC/HEIF → PNG(浏览器原生通常不支持,走 heic2any)----
  // 加载策略:
  //   1. Extension 环境:从 chrome-extension vendor 本地 <script> 注入(CSP 允许)
  //      脚本在 MAIN world 执行,heic2any 挂在 MAIN world 的 window 上,
  //      内容脚本(ISOLATED world)通过 unsafeWindow 访问。
  //   2. Userscript 环境:GM_xmlhttpRequest 拉脚本文本,Blob URL 注入 <script>
  //      (CSP 可能拦截 blob:,此时不可用)
  let heic2anyApiPromise = null;
  function _getHeic2Any() {
    // <script> 注入后 heic2any 挂在 MAIN world 的 window 上
    // 内容脚本在 ISOLATED world,需要跨 world 访问
    // 优先级:document.defaultView(标准 MAIN world window)> unsafeWindow > window
    const mainWin = typeof document !== 'undefined' && document.defaultView;
    if (mainWin && typeof mainWin.heic2any === 'function') return mainWin.heic2any;
    if (typeof unsafeWindow !== 'undefined' && typeof unsafeWindow.heic2any === 'function') return unsafeWindow.heic2any;
    if (typeof window.heic2any === 'function') return window.heic2any;
    // 调试:看看 heic2any 到底挂在哪
    if (mainWin) console.log('[heic2any] mainWin.heic2any:', typeof mainWin.heic2any, 'keys:', mainWin.heic2any ? Object.keys(mainWin.heic2any).slice(0,5) : 'N/A');
    console.log('[heic2any] unsafeWindow.heic2any:', typeof unsafeWindow !== 'undefined' ? typeof unsafeWindow.heic2any : 'N/A', 'window.heic2any:', typeof window.heic2any);
    return null;
  }
  async function ensureHeic2AnyLoaded() {
    const existing = _getHeic2Any();
    if (existing) return existing;
    if (heic2anyApiPromise) return heic2anyApiPromise;
    const runtime = getXDexRuntimeInfo();
    const isExtension = runtime && runtime.kind === 'extension';
    heic2anyApiPromise = (async () => {
      // Extension:通过 loader 桥接加载(解决 MAIN world ↔ ISOLATED world 隔离问题)
      // heic2any-loader.js 在 MAIN world 加载 heic2any 并通过 postMessage 提供转换服务
      if (isExtension) {
        const loaderUrl = chrome.runtime.getURL('vendor/heic2any/heic2any-loader.js');
        await new Promise((resolve, reject) => {
          const existing = document.querySelector(`script[data-xdex-heic2any-loader="1"]`);
          if (existing) {
            // 已注入,检查是否就绪
            const ready = document.documentElement.getAttribute('data-xdex-heic2any-ready');
            if (ready === 'ready') { resolve(); return; }
            if (ready === 'error') { reject(new Error('heic2any 桥接加载失败')); return; }
          }
          const s = document.createElement('script');
          s.src = loaderUrl;
          s.dataset.xdexHeic2anyLoader = '1';
          s.onload = () => {
            // loader 异步加载 heic2any.min.js,通过 DOM 属性信号就绪
            let waited = 0;
            const poll = () => {
              const attr = document.documentElement.getAttribute('data-xdex-heic2any-ready');
              if (attr === 'ready') { resolve(); return; }
              if (attr === 'error') { reject(new Error('heic2any 桥接加载失败')); return; }
              if (++waited > 50) { reject(new Error('heic2any 桥接加载超时(5s)')); return; }
              setTimeout(poll, 100);
            };
            poll();
          };
          s.onerror = () => reject(new Error('heic2any-loader.js 加载失败'));
          document.documentElement.appendChild(s);
        });
        // Extension 走 postMessage 桥接,不直接返回函数
        return '__xdex_bridge__';
      }
      // Userscript:用 GM_xmlhttpRequest 拉脚本文本,Blob URL 注入
      if (typeof GM_xmlhttpRequest === 'function') {
        const scriptUrl = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/heic2any.min.js';
        const scriptText = await new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: 'GET',
            url: scriptUrl,
            onload: (resp) => {
              if (resp.status >= 200 && resp.status < 300 && resp.responseText) resolve(resp.responseText);
              else reject(new Error(`heic2any 下载失败: HTTP ${resp.status}`));
            },
            onerror: () => reject(new Error('heic2any 下载失败,请检查网络'))
          });
        });
        // 临时屏蔽 define,强制 heic2any UMD 走 self.heic2any 路径(同 Extension 逻辑)
        const wrappedText = 'var __xdexSavedDefine=window.define;if(typeof __xdexSavedDefine!=="undefined")window.define=void 0;\n'
          + scriptText
          + '\nif(typeof __xdexSavedDefine!=="undefined")window.define=__xdexSavedDefine;';
        const blob = new Blob([wrappedText], { type: 'application/javascript' });
        const blobUrl = URL.createObjectURL(blob);
        await new Promise((resolve, reject) => {
          const s = document.createElement('script');
          s.src = blobUrl;
          s.dataset.xdexHeic2any = '1';
          s.onload = () => { URL.revokeObjectURL(blobUrl); resolve(); };
          s.onerror = () => { URL.revokeObjectURL(blobUrl); reject(new Error('heic2any Blob 加载失败')); };
          document.documentElement.appendChild(s);
        });
        const fn = _getHeic2Any();
        if (fn) return fn;
        throw new Error('heic2any 已加载但未导出可用函数');
      }
      throw new Error('无法加载 heic2any:无可用加载方式');
    })().catch((err) => {
      heic2anyApiPromise = null;
      throw err;
    });
    return heic2anyApiPromise;
  }
  async function convertHeicToPng(file) {
    // 优先尝试原生解码(少数浏览器/系统可能支持)
    try {
      if (typeof createImageBitmap === 'function') {
        const bitmap = await createImageBitmap(file);
        const canvas = document.createElement('canvas');
        canvas.width = bitmap.width;
        canvas.height = bitmap.height;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(bitmap, 0, 0);
        if (typeof bitmap.close === 'function') bitmap.close();
        const pngBlob = await new Promise((resolve, reject) => {
          canvas.toBlob((b) => b ? resolve(b) : reject(new Error('HEIC→PNG toBlob 失败')), 'image/png');
        });
        return new File([pngBlob], file.name.replace(/\.(heic|heif|hif)$/i, '.png'), {
          type: 'image/png',
          lastModified: Date.now()
        });
      }
    } catch (nativeErr) {
      console.warn('[convertHeicToPng] 原生解码失败,改用 heic2any:', nativeErr && nativeErr.message);
    }
    const heic2any = await ensureHeic2AnyLoaded();
    let pngBlob;
    if (heic2any === '__xdex_bridge__') {
      // Extension:通过 postMessage 桥接调用 MAIN world 的 heic2any
      const arrayBuffer = await file.arrayBuffer();
      const requestId = 'heic2any_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
      pngBlob = await new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
          window.removeEventListener('message', onResult);
          reject(new Error('heic2any 桥接转换超时(30s)'));
        }, 30000);
        function onResult(e) {
          if (!e.data || e.data.type !== 'xdex-heic2any-result' || e.data.requestId !== requestId) return;
          clearTimeout(timeout);
          window.removeEventListener('message', onResult);
          if (e.data.success) {
            resolve(new Blob([e.data.arrayBuffer], { type: 'image/png' }));
          } else {
            reject(new Error('heic2any 转换失败: ' + (e.data.error || '未知')));
          }
        }
        window.addEventListener('message', onResult);
        // transfer ArrayBuffer 零拷贝发给 MAIN world
        window.postMessage({
          type: 'xdex-heic2any-convert',
          requestId: requestId,
          arrayBuffer: arrayBuffer
        }, '*', [arrayBuffer]);
      });
    } else {
      // Userscript:直接调用 heic2any 函数
      const result = await heic2any({
        blob: file,
        toType: 'image/png',
        quality: 1,
        multiple: false
      });
      pngBlob = Array.isArray(result) ? result[0] : result;
    }
    if (!(pngBlob instanceof Blob)) {
      throw new Error('HEIC 转换未返回有效 PNG');
    }
    return new File([pngBlob], file.name.replace(/\.(heic|heif|hif)$/i, '.png'), {
      type: 'image/png',
      lastModified: Date.now()
    });
  }
  // ---- WebP 格式转换:静态 WebP → PNG,动态 WebP → APNG ----
  async function convertWebpToAcceptedFormat(file, isAnimated) {
    if (!isAnimated) {
      // 静态 WebP → PNG:直接用 canvas 重绘
      const bitmap = await createImageBitmap(file);
      const canvas = document.createElement('canvas');
      canvas.width = bitmap.width;
      canvas.height = bitmap.height;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(bitmap, 0, 0);
      bitmap.close();
      const pngBlob = await new Promise((resolve, reject) => {
        canvas.toBlob((b) => b ? resolve(b) : reject(new Error('WebP→PNG toBlob 失败')), 'image/png');
      });
      return new File([pngBlob], file.name.replace(/\.webp$/i, '.png'), { type: 'image/png', lastModified: Date.now() });
    }
    // 动态 WebP → APNG:用 ImageDecoder API 逐帧解码,保留动画
    return convertAnimatedWebpToApng(file);
  }
  const STATIC_MAX_SIZE_KB = 2048;
  const STATIC_TARGET_LOWER_KB = 1900;
  const STATIC_ACCEPTABLE_LOWER_KB = 1850;
  const STATIC_MAX_ATTEMPTS = 5;
  async function compressImageToSize(file, maxSizeKB = STATIC_MAX_SIZE_KB, minQuality = 0.6, detectedFormat) {
    const maxSizeBytes = STATIC_MAX_SIZE_KB * 1024;
    const targetUpperBytes = maxSizeBytes;
    const targetLowerBytes = STATIC_TARGET_LOWER_KB * 1024;
    if (file.size <= maxSizeBytes) {
      return file;
    }
    console.log(`[compressImage] 开始压缩: ${(file.size / 1024).toFixed(1)}KB -> 目标区间 ${(targetLowerBytes / 1024).toFixed(0)}-${maxSizeKB}KB`);
    const startTime = performance.now();
    return new Promise((resolve, reject) => {
      const img = new Image();
      const url = URL.createObjectURL(file);
      img.onload = async () => {
        URL.revokeObjectURL(url);
        // 优先使用外部传入的真实格式(magic bytes 检测),避免 file.type 被后缀名误导
        // WebP 和 animated-webp 统一输出为 PNG(服务器不接受 WebP)
        const originalType = detectedFormat
          ? (detectedFormat === 'png' ? 'image/png' : detectedFormat === 'jpeg' ? 'image/jpeg' : (detectedFormat === 'webp' || detectedFormat === 'animated-webp') ? 'image/png' : file.type || 'image/jpeg')
          : (file.type || 'image/jpeg');
        const outputType = getStaticImageOutputType(originalType);
        const drawToCanvas = (scale) => {
          const canvas = document.createElement('canvas');
          const ctx = canvas.getContext('2d');
          canvas.width = Math.max(1, Math.floor(img.width * scale));
          canvas.height = Math.max(1, Math.floor(img.height * scale));
          ctx.imageSmoothingEnabled = true;
          ctx.imageSmoothingQuality = 'high';
          ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
          return canvas;
        };
        const canvasToBlob = (canvas, quality) => new Promise((resolveBlob, rejectBlob) => {
          canvas.toBlob((blob) => {
            if (!blob) {
              rejectBlob(new Error('图片压缩失败'));
              return;
            }
            resolveBlob(blob);
          }, outputType, quality);
        });
        const toFile = (blob) => new File([blob], file.name, {
          type: outputType,
          lastModified: Date.now()
        });
        const formatSupportsQuality = outputType !== 'image/png';
        const clampScale = (scale, minScale = 0.12, maxScale = 1) => Math.min(maxScale, Math.max(minScale, scale));
        let bestBlob = null;
        let bestScore = Infinity;
        let totalAttempts = 0;
        const considerBlob = (blob) => {
          if (blob.size > targetUpperBytes) return;
          const score = targetUpperBytes - blob.size;
          if (score < bestScore) {
            bestScore = score;
            bestBlob = blob;
          }
        };
        const searchBestQualityAtScale = async (scale) => {
          const canvas = drawToCanvas(scale);
          if (!formatSupportsQuality) {
            totalAttempts++;
            const blob = await canvasToBlob(canvas, 0.92);
            console.log(`[compressImage] 尝试#${totalAttempts}: 缩放=${scale.toFixed(3)}, 质量=png-default -> ${(blob.size / 1024).toFixed(1)}KB`);
            considerBlob(blob);
            return { blob, exceeded: blob.size > targetUpperBytes, hitRange: blob.size >= targetLowerBytes && blob.size <= targetUpperBytes };
          }
          let low = minQuality;
          let high = 0.95;
          let localBest = null;
          let localBestScore = Infinity;
          for (let i = 0; i < STATIC_MAX_ATTEMPTS; i++) {
            const quality = (low + high) / 2;
            totalAttempts++;
            const blob = await canvasToBlob(canvas, quality);
            const sizeKB = blob.size / 1024;
            console.log(`[compressImage] 尝试#${totalAttempts}: 缩放=${scale.toFixed(3)}, 质量=${quality.toFixed(4)} -> ${sizeKB.toFixed(1)}KB`);
            if (blob.size <= targetUpperBytes) {
              const score = targetUpperBytes - blob.size;
              if (score < localBestScore) {
                localBestScore = score;
                localBest = blob;
              }
              considerBlob(blob);
              low = quality;
            } else {
              high = quality;
            }
          }
            // ✨ 精细搜索:从 high(≈0.95) 逐步提升到 1.0,寻找最接近限制的质量
            if (localBest && localBest.size < targetLowerBytes) {
              console.log(`[compressImage] 精细搜索:当前最佳 ${(localBest.size / 1024).toFixed(1)}KB < ${(STATIC_TARGET_LOWER_KB)}KB,尝试提高质量……`);
              const fineQualities = [0.96, 0.965, 0.97, 0.975, 0.98, 0.985, 0.99, 0.995, 0.998];
              for (const q of fineQualities) {
              totalAttempts++;
              const blob = await canvasToBlob(canvas, q);
              const sizeKB = blob.size / 1024;
              console.log(`[compressImage] 尝试#${totalAttempts}: 缩放=${scale.toFixed(3)}, 质量=${q.toFixed(4)}(精细) -> ${sizeKB.toFixed(1)}KB`);
              considerBlob(blob);
              if (blob.size <= targetUpperBytes) {
                const score = targetUpperBytes - blob.size;
                if (score < localBestScore) {
                  localBestScore = score;
                  localBest = blob;
                }
                if (blob.size >= targetLowerBytes) {
                  return {
                    blob: localBest,
                    exceeded: false,
                    hitRange: true
                  };
                }
              } else {
                break;
              }
            }
          }
          if (localBest) {
            return {
              blob: localBest,
              exceeded: false,
              hitRange: localBest.size >= targetLowerBytes && localBest.size <= targetUpperBytes
            };
          }
          const fallbackBlob = await canvasToBlob(canvas, minQuality);
          totalAttempts++;
          console.log(`[compressImage] 尝试#${totalAttempts}: 缩放=${scale.toFixed(3)}, 质量=${minQuality.toFixed(4)}(fallback) -> ${(fallbackBlob.size / 1024).toFixed(1)}KB`);
          considerBlob(fallbackBlob);
          return {
            blob: fallbackBlob,
            exceeded: fallbackBlob.size > targetUpperBytes,
            hitRange: fallbackBlob.size >= targetLowerBytes && fallbackBlob.size <= targetUpperBytes
          };
        };
        const searchBestPngScale = async () => {
          const relaxedLowerBytes = Math.max(Math.floor(maxSizeBytes * 0.84), Math.floor(targetLowerBytes * 0.88));
          const baseScale = Math.sqrt(targetUpperBytes / file.size);
          // 硬下限:只防过小。不要用 baseScale*0.7 当下限——canvas 重编码后体积常比原文件更差,
          // 否则会像 21MB PNG 那样把预估 scale(≈0.17) 卡在 0.215 动不了。
          const HARD_MIN_SCALE = 0.05;
          let minScale = HARD_MIN_SCALE;
          let scale = clampScale(baseScale * 1.08, minScale, 0.92);
          let lastUnderLimitBlob = null;
          const seenScales = new Set();
          for (let phase = 0; phase < 6; phase++) {
            const scaleKey = scale.toFixed(4);
            if (seenScales.has(scaleKey)) break;
            seenScales.add(scaleKey);
            const result = await searchBestQualityAtScale(scale);
            if (!result.exceeded) lastUnderLimitBlob = result.blob;
            if (result.hitRange) return result;
            if (result.exceeded) {
              const remain = 6 - phase - 1;
              const ratio = result.blob.size / targetUpperBytes;
              let safety = 0.95;
              if (ratio > 2.0 || remain <= 0) safety = 0.85;
              else if (ratio > 1.4 || remain <= 1) safety = 0.90;
              const predicted = scale * Math.sqrt(targetUpperBytes / Math.max(result.blob.size, 1)) * safety;
              // 预估低于当前下限时下调下限,避免 clamp 把步进掐死
              if (predicted < minScale) {
                minScale = clampScale(predicted * 0.95, HARD_MIN_SCALE, minScale);
              }
              let nextScale = clampScale(predicted, minScale, scale * 0.98);
              // 仍几乎不动但还超限:强制再砍一刀
              if (Math.abs(nextScale - scale) < 0.005) {
                const forced = clampScale(predicted * 0.9, HARD_MIN_SCALE, scale * 0.9);
                if (forced < scale - 0.005) {
                  nextScale = forced;
                  minScale = Math.min(minScale, forced);
                } else {
                  console.log(`[compressImage] PNG超限但无法继续缩小: ratio=${ratio.toFixed(2)}, scale=${scale.toFixed(3)}, predicted=${predicted.toFixed(3)}`);
                  break;
                }
              }
              console.log(`[compressImage] PNG超限 ratio=${ratio.toFixed(2)}, remain=${remain}, scale ${scale.toFixed(3)} -> ${nextScale.toFixed(3)} (safety=${safety}, min=${minScale.toFixed(3)})`);
              scale = nextScale;
              continue;
            }
            if (result.blob.size >= relaxedLowerBytes) {
              return {
                blob: result.blob,
                exceeded: false,
                hitRange: false
              };
            }
            const growRatio = Math.sqrt(targetUpperBytes / Math.max(result.blob.size, 1));
            const nextScale = clampScale(scale * Math.min(growRatio * 0.985, 1.12), minScale, 0.98);
            if (nextScale <= scale + 0.004 || nextScale >= 0.995) break;
            scale = nextScale;
          }
          if (bestBlob) {
            return {
              blob: bestBlob,
              exceeded: false,
              hitRange: bestBlob.size >= targetLowerBytes && bestBlob.size <= targetUpperBytes
            };
          }
          if (lastUnderLimitBlob) {
            return {
              blob: lastUnderLimitBlob,
              exceeded: false,
              hitRange: lastUnderLimitBlob.size >= targetLowerBytes && lastUnderLimitBlob.size <= targetUpperBytes
            };
          }
          return {
            blob: null,
            exceeded: true,
            hitRange: false
          };
        };
        try {
          if (!formatSupportsQuality) {
            const pngResult = await searchBestPngScale();
            const finalBlob = pngResult.blob || bestBlob;
            if (finalBlob) {
              const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
              console.log(`[compressImage] ✓ PNG完成: ${(file.size / 1024).toFixed(1)}KB -> ${(finalBlob.size / 1024).toFixed(1)}KB, ${totalAttempts}次尝试, ${elapsed}秒`);
              resolve(toFile(finalBlob));
              return;
            }
            reject(new Error('无法将PNG压缩到限制以内'));
            return;
          }
          let result = await searchBestQualityAtScale(1);
          if (result.hitRange) {
            const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
            console.log(`[compressImage] ✓ 原尺寸命中目标: ${(bestBlob.size / 1024).toFixed(1)}KB, ${totalAttempts}次尝试, ${elapsed}秒`);
            resolve(toFile(bestBlob));
            return;
          }
          if (result.exceeded) {
            // 实测体积反推 scale(∝√面积);下限放宽到 0.2,避免卡在 0.5 仍超限
            const STATIC_MIN_SCALE = 0.2;
            const maxPhases = 4;
            let scale = clampScale(Math.sqrt(targetUpperBytes / result.blob.size) * 0.95, STATIC_MIN_SCALE, 0.98);
            console.log(`[compressImage] 原尺寸超限,预估缩放=${scale.toFixed(3)}`);
            for (let phase = 0; phase < maxPhases; phase++) {
              result = await searchBestQualityAtScale(scale);
              if (result.hitRange) break;
              if (result.exceeded) {
                const remain = maxPhases - phase - 1;
                const ratio = result.blob.size / targetUpperBytes;
                // 安全系数:剩余次数少 / 超限倍率大时更狠,避免次数耗尽仍超
                let safety = 0.95;
                if (ratio > 2.0 || remain <= 0) safety = 0.85;
                else if (ratio > 1.35 || remain <= 1) safety = 0.90;
                const predicted = scale * Math.sqrt(targetUpperBytes / Math.max(result.blob.size, 1)) * safety;
                const nextScale = clampScale(predicted, STATIC_MIN_SCALE, scale * 0.98);
                console.log(`[compressImage] 超限 ratio=${ratio.toFixed(2)}, remain=${remain}, scale ${scale.toFixed(3)} -> ${nextScale.toFixed(3)} (safety=${safety})`);
                if (Math.abs(nextScale - scale) < 0.004) break;
                scale = nextScale;
              } else if (bestBlob && bestBlob.size < targetLowerBytes) {
                scale = clampScale(scale * 1.03, STATIC_MIN_SCALE, 1);
                if (scale >= 0.999) break;
              } else {
                break;
              }
            }
          }
          if (bestBlob) {
            const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
            console.log(`[compressImage] ✓ 完成: ${(file.size / 1024).toFixed(1)}KB -> ${(bestBlob.size / 1024).toFixed(1)}KB, ${totalAttempts}次尝试, ${elapsed}秒`);
            resolve(toFile(bestBlob));
            return;
          }
          reject(new Error('无法将图片压缩到目标大小'));
        } catch (err) {
          reject(err);
        }
      };
      img.onerror = () => {
        URL.revokeObjectURL(url);
        reject(new Error('图片加载失败'));
      };
      img.src = url;
    });
  }
  const GIF_MAX_SIZE_KB = 2048;
  const GIF_TARGET_LOWER_KB = 1900;
  const GIF_ACCEPTABLE_LOWER_KB = 1850;
  const GIF_MAX_ORIGINAL_KB = 30 * 1024;//目前限制30MB以内
  const GIF_MAX_LONG_EDGE = 1600;
  const GIF_MAX_ATTEMPTS = 3;//目前最多重试3次
  let gifsicleApiPromise = null;
  function clampNumber(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }
  function formatKB(sizeBytes) {
    return `${(sizeBytes / 1024).toFixed(1)}KB`;
  }
  function getGifDimensions(file) {
    return new Promise((resolve) => {
      const url = URL.createObjectURL(file);
      const img = new Image();
      img.onload = () => {
        const result = {
          width: img.naturalWidth || img.width || 0,
          height: img.naturalHeight || img.height || 0
        };
        URL.revokeObjectURL(url);
        resolve(result);
      };
      img.onerror = () => {
        URL.revokeObjectURL(url);
        resolve({ width: 0, height: 0 });
      };
      img.src = url;
    });
  }
  async function ensureGifsicleLoaded() {
    if (window.__xdexGifsicleModule) {
      return window.__xdexGifsicleModule;
    }
    if (!gifsicleApiPromise) {
      gifsicleApiPromise = import('https://cdn.jsdelivr.net/npm/gifsicle-wasm-browser/dist/gifsicle.min.js')
        .then((mod) => {
          const api = (mod && (mod.default || mod.gifsicle || mod)) || null;
          if (!api) {
            throw new Error('GIF压缩库未返回可用模块');
          }
          window.__xdexGifsicleModule = api;
          return api;
        })
        .catch((err) => {
          gifsicleApiPromise = null;
          throw err;
        });
    }
    return gifsicleApiPromise;
  }
  async function runGifsicleAttempt(api, inputFile, args) {
    const fileName = '/tem/input.gif';
    const outName = '/out/out.gif';
    const commandText = [...args, fileName, '-o', outName].join(' ');
    if (typeof api.run === 'function') {
      const result = await api.run({
        input: [{ file: inputFile, name: fileName }],
        command: [commandText]
      });
      const output = (result && (result.output || result.files || result)) || [];
      const outFile = Array.isArray(output)
        ? output.find((item) => item && (((item.name || '') === outName) || /out\.gif$/i.test(item.name || '')))
        : null;
      const data = outFile && (outFile.data || outFile.buffer || outFile.content || outFile.file || outFile.blob);
      if (data instanceof Blob) return data;
      if (data instanceof Uint8Array) return new Blob([data], { type: 'image/gif' });
      if (data instanceof ArrayBuffer) return new Blob([new Uint8Array(data)], { type: 'image/gif' });
      if (outFile instanceof Blob) return outFile;
      throw new Error('gifsicle.run 未返回可用输出');
    }
    if (api.default && typeof api.default.run === 'function') {
      return runGifsicleAttempt(api.default, inputFile, args);
    }
    throw new Error('未识别的 gifsicle-wasm-browser API 形态');
  }
  // GIF 结构修复:用 gifsicle 重新编码(不压缩,只修复 GCT 等结构问题)
  async function reencodeGifWithGifsicle(file) {
    const api = await ensureGifsicleLoaded();
    const blob = await runGifsicleAttempt(api, file, ['--no-warnings']);
    return new File([blob], file.name.replace(/\.gif$/i, '.gif'), { type: 'image/gif', lastModified: Date.now() });
  }
  async function compressGifToSize(file, options = {}) {
    const now = () => (typeof performance !== 'undefined' && performance && typeof performance.now === 'function')
      ? performance.now()
      : Date.now();
    const finiteNumber = (value, digits = 2) => Number.isFinite(value) ? Number(value.toFixed(digits)) : 0;
    const totalStartTime = now();
    let stage = '初始化';
    let completedAttempts = 0;
    try {
    const maxSizeKB = options.maxSizeKB || GIF_MAX_SIZE_KB;
    const targetLowerKB = options.targetLowerKB || GIF_TARGET_LOWER_KB;
    const acceptableLowerKB = options.acceptableLowerKB || GIF_ACCEPTABLE_LOWER_KB;
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    const maxBytes = maxSizeKB * 1024;
    const lowerBytes = targetLowerKB * 1024;
    const acceptableLowerBytes = acceptableLowerKB * 1024;
    const originalKB = file.size / 1024;
    if (file.size <= maxBytes) {
      const totalElapsedMs = finiteNumber(now() - totalStartTime);
      console.log('[X岛-EX][GIF压缩][汇总]', {
        totalElapsedMs,
        completedAttempts: 0,
        originalKB: finiteNumber(originalKB, 1),
        finalKB: finiteNumber(originalKB, 1),
        compressionRate: 1,
        reductionPercent: 0,
        finalLossy: null,
        finalColors: null,
        finalScale: null,
        finalCommand: '无需压缩'
      });
      return {
        file,
        summary: '原始 GIF 已在目标大小内,无需压缩',
        attempts: [],
        durationMs: totalElapsedMs
      };
    }
    if (originalKB > GIF_MAX_ORIGINAL_KB) {
      throw new Error(`GIF 原始体积过大(${formatKB(file.size)}),限制为 <= ${GIF_MAX_ORIGINAL_KB}KB`);
    }
    stage = '读取GIF尺寸';
    const dimensionsStartTime = now();
    const { width, height } = await getGifDimensions(file);
    const dimensionsElapsedMs = finiteNumber(now() - dimensionsStartTime);
    if (width > 0 && height > 0) {
      const longEdge = Math.max(width, height);
      if (longEdge > GIF_MAX_LONG_EDGE) {
        throw new Error(`GIF 尺寸过大(${width}x${height}),限制长边 <= ${GIF_MAX_LONG_EDGE}`);
      }
    }
    stage = '加载gifsicle模块';
    const moduleLoadStartTime = now();
    const api = await ensureGifsicleLoaded();
    const moduleLoadElapsedMs = finiteNumber(now() - moduleLoadStartTime);
    const attempts = [];
    let bestBlob = null;
    let bestArgs = null;
    let bestAttemptLog = null;
    const oversizeRatio = file.size / maxBytes;
    let scale = clampNumber(Math.sqrt(maxBytes / file.size) * (oversizeRatio > 2.5 ? 1.32 : (oversizeRatio > 1.6 ? 1.18 : 1.06)), 0.22, 0.98);
    let lossy = oversizeRatio > 2.5 ? 40 : (oversizeRatio > 1.6 ? 30 : 20);
    let colors = oversizeRatio > 2.5 ? 128 : 160;
    const seenConfigs = new Set();
    console.log(`[compressGif] 开始压缩: ${formatKB(file.size)} -> 目标区间 ${targetLowerKB}-${maxSizeKB}KB, 倍率=${oversizeRatio.toFixed(2)}`);
    for (let i = 0; i < GIF_MAX_ATTEMPTS; i++) {
      const args = ['-O3', `--lossy=${Math.round(lossy)}`, '--colors', String(Math.round(colors))];
      if (scale < 0.995) {
        args.push('--scale', scale.toFixed(3));
      }
      const configKey = `${Math.round(lossy)}|${Math.round(colors)}|${scale < 0.995 ? scale.toFixed(3) : '1.000'}`;
      if (seenConfigs.has(configKey)) {
        scale = clampNumber(scale * 0.97, 0.16, 0.98);
        lossy = clampNumber(Math.round(lossy) + 8, 0, 200);
        colors = clampNumber(Math.round(colors) - 16, 48, 256);
        continue;
      }
      seenConfigs.add(configKey);
      const attemptStartTime = now();
      stage = `gifsicle压缩第${i + 1}轮`;
      try {
        const blob = await runGifsicleAttempt(api, file, args);
        completedAttempts++;
        const sizeKB = blob.size / 1024;
        const hitTarget = blob.size <= maxBytes;
        const inRange = blob.size >= lowerBytes && blob.size <= maxBytes;
        const acceptable = hitTarget && blob.size >= acceptableLowerBytes;
        const command = [...args, '/tem/input.gif', '-o', '/out/out.gif'].join(' ');
        const attemptLog = {
          index: i + 1,
          args: args.join(' '),
          sizeKB: Number(sizeKB.toFixed(1)),
          hitTarget,
          inRange
        };
        attempts.push(attemptLog);
        console.log('[X岛-EX][GIF压缩][轮次]', {
          attempt: `${attemptLog.index}/${GIF_MAX_ATTEMPTS}`,
          inputKB: finiteNumber(originalKB, 1),
          outputKB: finiteNumber(sizeKB, 1),
          compressionRate: finiteNumber(blob.size / file.size, 4),
          reductionPercent: finiteNumber((1 - blob.size / file.size) * 100, 2),
          lossy: Math.round(lossy),
          colors: Math.round(colors),
          scale: scale < 0.995 ? finiteNumber(scale, 3) : 1,
          command,
          elapsedMs: finiteNumber(now() - attemptStartTime),
          hitTargetRange: inRange,
          acceptable: acceptable,
          overLimit: !hitTarget
        });
        if (onProgress) {
          onProgress({
            index: attemptLog.index,
            total: GIF_MAX_ATTEMPTS,
            originalKB: Number(originalKB.toFixed(1)),
            currentKB: attemptLog.sizeKB,
            args: attemptLog.args,
            hitTarget,
            inRange
          });
        }
        if (hitTarget) {
          if (!bestBlob || blob.size > bestBlob.size) {
            bestBlob = blob;
            bestArgs = args;
            bestAttemptLog = {
              lossy: Math.round(lossy),
              colors: Math.round(colors),
              scale: scale < 0.995 ? finiteNumber(scale, 3) : 1,
              command
            };
          }
          if (inRange || blob.size >= acceptableLowerBytes) {
            break;
          }
          scale = clampNumber(scale * 1.08, 0.16, 0.98);
          lossy = clampNumber(Math.round(lossy) - 12, 0, 200);
          colors = clampNumber(Math.round(colors) + 32, 48, 256);
        } else {
          // 默认温和步进;剩余次数不足或超限倍率大时,按实测体积做 scale² 激进跳变
          const remain = GIF_MAX_ATTEMPTS - (i + 1);
          const ratio = blob.size / maxBytes;
          let nextScale = scale * 0.90;
          let nextLossy = Math.round(lossy) + 18;
          let nextColors = Math.round(colors) - 24;
          if (ratio > 1.45 || (remain <= 1 && ratio > 1.15) || (remain <= 0 && ratio > 1.05)) {
            const safety = remain <= 1 ? 0.88 : 0.92;
            const predictedScale = scale * Math.sqrt(maxBytes / Math.max(blob.size, 1)) * safety;
            nextScale = Math.min(nextScale, predictedScale);
            nextLossy = Math.max(nextLossy, Math.round(lossy) + (remain <= 1 ? 40 : 28));
            nextColors = Math.min(nextColors, Math.round(colors) - (remain <= 1 ? 48 : 32));
            console.log(`[compressGif] 激进跳变: ratio=${ratio.toFixed(2)}, remain=${remain}, scale->${nextScale.toFixed(3)}, lossy->${nextLossy}, colors->${nextColors}`);
          }
          scale = clampNumber(nextScale, 0.16, 0.98);
          lossy = clampNumber(nextLossy, 0, 200);
          colors = clampNumber(nextColors, 48, 256);
        }
      } catch (error) {
        completedAttempts++;
        const message = error && error.message ? error.message : String(error);
        attempts.push({ index: i + 1, args: args.join(' '), error: message });
        console.error('[X岛-EX][GIF压缩][失败]', {
          stage: stage,
          elapsedMs: finiteNumber(now() - totalStartTime),
          completedAttempts: completedAttempts,
          attempt: `${i + 1}/${GIF_MAX_ATTEMPTS}`,
          command: [...args, '/tem/input.gif', '-o', '/out/out.gif'].join(' '),
          error: message
        });
        if (onProgress) {
          onProgress({
            index: i + 1,
            total: GIF_MAX_ATTEMPTS,
            originalKB: Number(originalKB.toFixed(1)),
            currentKB: null,
            args: args.join(' '),
            error: message,
            hitTarget: false,
            inRange: false
          });
        }
        // 失败轮次也按剩余次数适度加压,避免空耗预算
        const remain = GIF_MAX_ATTEMPTS - (i + 1);
        scale = clampNumber(scale * (remain <= 1 ? 0.82 : 0.90), 0.16, 0.98);
        lossy = clampNumber(Math.round(lossy) + (remain <= 1 ? 28 : 18), 0, 200);
        colors = clampNumber(Math.round(colors) - (remain <= 1 ? 32 : 24), 48, 256);
      }
    }
    if (!bestBlob) {
      throw new Error('GIF 压缩后仍无法降到 2048KB 以下');
    }
    const compressedFile = new File([bestBlob], file.name.replace(/\.gif$/i, '') + '-compressed.gif', {
      type: 'image/gif',
      lastModified: Date.now()
    });
    const summary = `最佳命令: ${bestArgs.join(' ')};原始 ${formatKB(file.size)} -> 压缩后 ${formatKB(compressedFile.size)}`;
    const durationMs = finiteNumber(now() - totalStartTime);
    console.log('[X岛-EX][GIF压缩][汇总]', {
      totalElapsedMs: durationMs,
      dimensionsElapsedMs,
      moduleLoadElapsedMs,
      completedAttempts,
      originalKB: finiteNumber(originalKB, 1),
      finalKB: finiteNumber(compressedFile.size / 1024, 1),
      compressionRate: finiteNumber(compressedFile.size / file.size, 4),
      reductionPercent: finiteNumber((1 - compressedFile.size / file.size) * 100, 2),
      finalLossy: bestAttemptLog.lossy,
      finalColors: bestAttemptLog.colors,
      finalScale: bestAttemptLog.scale,
      finalCommand: bestAttemptLog.command
    });
    return { file: compressedFile, summary, attempts, durationMs };
    } catch (error) {
      const message = error && error.message ? error.message : String(error);
      console.error('[X岛-EX][GIF压缩][失败]', {
        stage: stage,
        elapsedMs: finiteNumber(now() - totalStartTime),
        completedAttempts: completedAttempts,
        error: message
      });
      throw error;
    }
  }
  // ---- APNG 组装工具(供 compressApngToSize 和 convertAnimatedWebpToApng 共用)----
  const _apngCrcTable = (() => {
    const table = new Uint32Array(256);
    for (let n = 0; n < 256; n++) {
      let c = n;
      for (let k = 0; k < 8; k++) {
        c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
      }
      table[n] = c;
    }
    return table;
  })();
  const _apngCrc32 = (data) => {
    let crc = 0xFFFFFFFF;
    for (let i = 0; i < data.length; i++) {
      crc = _apngCrcTable[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
    }
    return (crc ^ 0xFFFFFFFF) >>> 0;
  };
  const _apngReadPngChunks = (u8) => {
    const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
    let pos = 8; // 跳过 PNG signature
    const chunks = [];
    while (pos + 8 <= u8.length) {
      const len = dv.getUint32(pos); pos += 4;
      const type = String.fromCharCode(u8[pos], u8[pos+1], u8[pos+2], u8[pos+3]); pos += 4;
      if (pos + len + 4 > u8.length) break;
      chunks.push({ type, data: u8.slice(pos, pos + len) });
      pos += len + 4; // data + crc
    }
    return chunks;
  };
  const _apngBuild = (ihdrData, frameChunks, delays, numPlays) => {
    let totalSize = 8 + (12 + 13) + (12 + 8); // sig + IHDR + acTL
    for (let i = 0; i < frameChunks.length; i++) {
      totalSize += 12 + 26; // fcTL
      for (const idat of frameChunks[i]) {
        totalSize += (i === 0 ? 12 : 12 + 4) + idat.length; // IDAT vs fdAT
      }
    }
    totalSize += 12; // IEND
    const buf = new ArrayBuffer(totalSize);
    const u8 = new Uint8Array(buf);
    const dv = new DataView(buf);
    let o = 0;
    // PNG signature
    u8.set([137, 80, 78, 71, 13, 10, 26, 10], 0); o = 8;
    // IHDR
    dv.setUint32(o, 13); o += 4;
    u8.set([73, 72, 68, 82], o); o += 4;
    u8.set(ihdrData, o); o += 13;
    dv.setUint32(o, _apngCrc32(u8.slice(o - 4 - 13, o))); o += 4;
    // acTL
    dv.setUint32(o, 8); o += 4;
    u8.set([97, 99, 84, 76], o); o += 4;
    dv.setUint32(o, frameChunks.length); o += 4;
    dv.setUint32(o, numPlays); o += 4;
    dv.setUint32(o, _apngCrc32(u8.slice(o - 4 - 8, o))); o += 4;
    // Frames
    let seqNum = 0;
    for (let i = 0; i < frameChunks.length; i++) {
      const w = dv.getUint32(16);
      const h = dv.getUint32(20);
      const delay = delays[i] || 100;
      dv.setUint32(o, 26); o += 4;
      u8.set([102, 99, 84, 76], o); o += 4;
      dv.setUint32(o, seqNum++); o += 4;
      dv.setUint32(o, w); o += 4;
      dv.setUint32(o, h); o += 4;
      dv.setUint32(o, 0); o += 4;
      dv.setUint32(o, 0); o += 4;
      dv.setUint16(o, delay); o += 2;
      dv.setUint16(o, 1000); o += 2;
      // 全画布预合成帧:每帧都是完整 RGBA,必须 SOURCE 替换,不能 OVER 叠透明
      u8[o++] = 0; // dispose_op: NONE
      u8[o++] = 0; // blend_op: SOURCE(0=替换,保留本帧透明区)
      dv.setUint32(o, _apngCrc32(u8.slice(o - 4 - 26, o))); o += 4;
      if (i === 0) {
        for (const idatData of frameChunks[i]) {
          dv.setUint32(o, idatData.length); o += 4;
          u8.set([73, 68, 65, 84], o); o += 4;
          u8.set(idatData, o); o += idatData.length;
          dv.setUint32(o, _apngCrc32(u8.slice(o - 4 - idatData.length, o))); o += 4;
        }
      } else {
        for (const idatData of frameChunks[i]) {
          const fdatPayloadLen = 4 + idatData.length;
          dv.setUint32(o, fdatPayloadLen); o += 4;
          u8.set([102, 100, 65, 84], o); o += 4;
          dv.setUint32(o, seqNum++); o += 4;
          u8.set(idatData, o); o += idatData.length;
          dv.setUint32(o, _apngCrc32(u8.slice(o - 4 - idatData.length - 4, o))); o += 4;
        }
      }
    }
    // IEND
    dv.setUint32(o, 0); o += 4;
    u8.set([73, 69, 78, 68], o); o += 4;
    dv.setUint32(o, _apngCrc32(u8.slice(o - 4, o))); o += 4;
    return buf;
  };
  // ---- WebP ANMF bitstream → 单帧 WebP 容器包装 ----
  // ANMF 子 bitstream 已包含完整 chunk headers(ALPH+VP8 / VP8L / VP8),
  // 只需外包 RIFF/WEBP/VP8X。frameW/frameH 必须是真实像素尺寸(不是 Minus One)。
  //
  // 关键:RIFF size 字段 = 文件总长 - 8。之前写成 new Uint8Array(12 + size)
  // 会多出 4 字节尾零,Blob 长度 > RIFF 声明长度,浏览器解码器直接拒文件。
  function _wrapBitstreamAsWebp(bitstream, frameW, frameH) {
    const hasAlpha = (String.fromCharCode(bitstream[0], bitstream[1], bitstream[2], bitstream[3]) === 'ALPH')
                  || (bitstream[0] === 0x2F); // VP8L 自带 alpha
    const vp8x = new Uint8Array(10);
    if (hasAlpha) vp8x[0] = 0x10; // bit4 = has_alpha
    const w1 = Math.max(0, frameW - 1);
    const h1 = Math.max(0, frameH - 1);
    vp8x[4] = w1 & 0xFF; vp8x[5] = (w1 >> 8) & 0xFF; vp8x[6] = (w1 >> 16) & 0xFF;
    vp8x[7] = h1 & 0xFF; vp8x[8] = (h1 >> 8) & 0xFF; vp8x[9] = (h1 >> 16) & 0xFF;
    // layout: RIFF(4) + size(4) + WEBP(4) + VP8X_chunk(8+10=18) + bitstream(+pad)
    const pad = bitstream.length & 1; // RIFF 偶数对齐
    const payloadAfterRiffHeader = 4 + 18 + bitstream.length + pad; // = size 字段值
    const fileSize = 8 + payloadAfterRiffHeader; // 真实文件长度,必须精确
    const result = new Uint8Array(fileSize);
    const dv = new DataView(result.buffer, result.byteOffset, result.byteLength);
    result.set([0x52, 0x49, 0x46, 0x46], 0); // RIFF
    dv.setUint32(4, payloadAfterRiffHeader, true);
    result.set([0x57, 0x45, 0x42, 0x50], 8); // WEBP
    result.set([0x56, 0x50, 0x38, 0x58], 12); // VP8X
    dv.setUint32(16, 10, true);
    result.set(vp8x, 20);
    result.set(bitstream, 30);
    // pad 字节已由 new Uint8Array 填 0,无需再写
    return result;
  }
  // 解码单帧 WebP:createImageBitmap 优先,Image 兜底(比 ImageDecoder 对单帧更稳)
  async function _decodeSingleFrameWebp(frameBytes) {
    const blob = new Blob([frameBytes], { type: 'image/webp' });
    if (typeof createImageBitmap === 'function') {
      try {
        return await createImageBitmap(blob);
      } catch (e) {
        console.warn('[convertAnimatedWebpToApng] createImageBitmap 失败,尝试 Image:', e && e.message);
      }
    }
    return await new Promise((resolve, reject) => {
      const img = new Image();
      const url = URL.createObjectURL(blob);
      img.onload = () => {
        URL.revokeObjectURL(url);
        resolve(img);
      };
      img.onerror = () => {
        URL.revokeObjectURL(url);
        reject(new Error('Image 加载单帧 WebP 失败'));
      };
      img.src = url;
    });
  }
  // ---- 动态 WebP → APNG(保留动画:手动解析 ANMF + 逐帧解码合成)----
  async function convertAnimatedWebpToApng(file) {
    const buffer = await file.arrayBuffer();
    const src = new Uint8Array(buffer);
    const dv = new DataView(buffer);
    // 验证 RIFF/WEBP
    if (String.fromCharCode(src[0], src[1], src[2], src[3]) !== 'RIFF' ||
        String.fromCharCode(src[8], src[9], src[10], src[11]) !== 'WEBP') {
      throw new Error('不是有效的 WebP 文件');
    }
    // 解析 RIFF chunks:VP8X(画布尺寸)、ANIM(背景色/循环)、ANMF(帧数据)
    // WebP 规范:
    //   - VP8X / ANMF 的 width/height 存的是 Minus One(实际 = 值 + 1)
    //   - ANMF 的 x/y 存的是像素偏移 / 2(实际 = 值 * 2)
    let pos = 12;
    let canvasW = 0, canvasH = 0;
    let animBgColor = 0xFFFFFFFF; // WebP 默认白色不透明
    const anmfFrames = [];
    while (pos + 8 <= src.length) {
      const chunkId = String.fromCharCode(src[pos], src[pos+1], src[pos+2], src[pos+3]);
      const chunkLen = dv.getUint32(pos + 4, true); // little-endian
      const dataStart = pos + 8;
      if (chunkId === 'VP8X') {
        canvasW = (src[dataStart+4] | (src[dataStart+5] << 8) | (src[dataStart+6] << 16)) + 1;
        canvasH = (src[dataStart+7] | (src[dataStart+8] << 8) | (src[dataStart+9] << 16)) + 1;
      } else if (chunkId === 'ANIM') {
        // BGRA little-endian
        animBgColor = dv.getUint32(dataStart, true);
      } else if (chunkId === 'ANMF') {
        const xRaw = (src[dataStart]   | (src[dataStart+1] << 8) | (src[dataStart+2] << 16));
        const yRaw = (src[dataStart+3] | (src[dataStart+4] << 8) | (src[dataStart+5] << 16));
        const wRaw = (src[dataStart+6] | (src[dataStart+7] << 8) | (src[dataStart+8] << 16));
        const hRaw = (src[dataStart+9] | (src[dataStart+10] << 8) | (src[dataStart+11] << 16));
        const duration = (src[dataStart+12] | (src[dataStart+13] << 8) | (src[dataStart+14] << 16));
        const flags = src[dataStart+15];
        const disposal = (flags >> 1) & 1; // bit1: 0=none, 1=restore to bg
        const blending = flags & 1;        // bit0: 0=alpha blend, 1=no blend
        const bitstream = src.slice(dataStart + 16, dataStart + chunkLen);
        anmfFrames.push({
          x: xRaw * 2,
          y: yRaw * 2,
          w: wRaw + 1,
          h: hRaw + 1,
          duration: Math.max(duration || 100, 20),
          disposal,
          blending,
          bitstream
        });
      }
      pos = dataStart + chunkLen + (chunkLen & 1); // RIFF 2-byte aligned
    }
    if (anmfFrames.length === 0) {
      throw new Error('WebP 中未找到动画帧(ANMF)');
    }
    if (canvasW === 0 || canvasH === 0) {
      canvasW = anmfFrames[0].w;
      canvasH = anmfFrames[0].h;
    }
    console.log(`[convertAnimatedWebpToApng] 画布: ${canvasW}x${canvasH}, ${anmfFrames.length}帧, 首帧 ${anmfFrames[0].w}x${anmfFrames[0].h}`);
    // 逐帧解码 + 合成
    // 透明底处理:很多动画 WebP(尤其贴纸)ANIM 背景色写成不透明白 0xFFFFFFFF,
    // 但实际展示依赖帧 alpha。若按规范 dispose 到该背景色,透明底会被整块铺白。
    // 这里统一以透明画布合成,保留帧 alpha。
    const canvas = document.createElement('canvas');
    canvas.width = canvasW;
    canvas.height = canvasH;
    const ctx = canvas.getContext('2d', { willReadFrequently: true, alpha: true });
    const bgB = animBgColor & 0xFF;
    const bgG = (animBgColor >> 8) & 0xFF;
    const bgR = (animBgColor >> 16) & 0xFF;
    const bgA = (animBgColor >> 24) & 0xFF;
    // 仅当背景色本身带透明(A < 255)时才铺底;不透明白/黑一律当透明画布
    const useOpaqueBg = bgA > 0 && bgA < 255;
    if (bgA === 255) {
      console.log(`[convertAnimatedWebpToApng] ANIM 背景色 rgba(${bgR},${bgG},${bgB},${bgA}) 为不透明,改用透明画布以保留透明底`);
    }
    ctx.clearRect(0, 0, canvasW, canvasH);
    if (useOpaqueBg) {
      ctx.fillStyle = `rgba(${bgR},${bgG},${bgB},${bgA / 255})`;
      ctx.fillRect(0, 0, canvasW, canvasH);
    }
    const frames = [];
    let firstError = null;
    for (let fi = 0; fi < anmfFrames.length; fi++) {
      const frame = anmfFrames[fi];
      const frameWebpBytes = _wrapBitstreamAsWebp(frame.bitstream, frame.w, frame.h);
      let bitmap;
      try {
        bitmap = await _decodeSingleFrameWebp(frameWebpBytes);
      } catch (e) {
        if (!firstError) firstError = e;
        console.warn(`[convertAnimatedWebpToApng] 帧${fi}解码失败: ${frame.w}x${frame.h} @(${frame.x},${frame.y})`, e && e.message);
        continue;
      }
      // blending: 0 = alpha blend (OVER), 1 = no blend (SOURCE/replace)
      if (frame.blending === 1) {
        ctx.clearRect(frame.x, frame.y, frame.w, frame.h);
      }
      ctx.drawImage(bitmap, frame.x, frame.y);
      if (typeof bitmap.close === 'function') bitmap.close();
      // 捕获全画布 RGBA(含 alpha)
      const imgData = ctx.getImageData(0, 0, canvasW, canvasH);
      frames.push({ rgba: new Uint8Array(imgData.data), delay: frame.duration });
      // disposal: 1 = restore to background → 透明画布场景下恢复为透明
      if (frame.disposal === 1) {
        ctx.clearRect(frame.x, frame.y, frame.w, frame.h);
        if (useOpaqueBg) {
          ctx.fillStyle = `rgba(${bgR},${bgG},${bgB},${bgA / 255})`;
          ctx.fillRect(frame.x, frame.y, frame.w, frame.h);
        }
      }
    }
    if (frames.length === 0) {
      throw new Error('所有动画帧解码失败' + (firstError && firstError.message ? `(${firstError.message})` : ''));
    }
    console.log(`[convertAnimatedWebpToApng] 解码完成: ${frames.length}/${anmfFrames.length}帧`);
    // 逐帧渲染为 PNG 字节
    const framePngs = [];
    for (const f of frames) {
      const tmpCanvas = document.createElement('canvas');
      tmpCanvas.width = canvasW;
      tmpCanvas.height = canvasH;
      const tmpCtx = tmpCanvas.getContext('2d');
      tmpCtx.putImageData(new ImageData(new Uint8ClampedArray(f.rgba), canvasW, canvasH), 0, 0);
      tmpCtx.fillStyle = 'rgba(0,0,0,0.004)';
      tmpCtx.fillRect(canvasW - 1, canvasH - 1, 1, 1);
      const pngBlob = await new Promise((resolve, reject) => {
        tmpCanvas.toBlob((b) => b ? resolve(b) : reject(new Error('帧 PNG 编码失败')), 'image/png');
      });
      framePngs.push(new Uint8Array(await pngBlob.arrayBuffer()));
    }
    // 提取 IHDR 和 IDAT,组装 APNG
    const firstChunks = _apngReadPngChunks(framePngs[0]);
    const ihdrChunk = firstChunks.find(c => c.type === 'IHDR');
    if (!ihdrChunk) throw new Error('第一帧 PNG 缺少 IHDR');
    const allFrameIdats = framePngs.map(png => {
      const chunks = _apngReadPngChunks(png);
      return chunks.filter(c => c.type === 'IDAT').map(c => c.data);
    });
    const delays = frames.map(f => f.delay);
    const apngBuf = _apngBuild(ihdrChunk.data, allFrameIdats, delays, 0);
    console.log(`[convertAnimatedWebpToApng] APNG 组装完成: ${(apngBuf.byteLength / 1024).toFixed(1)}KB, ${frames.length}帧`);
    return new File([apngBuf], file.name.replace(/\.webp$/i, '.apng.png'), { type: 'image/png', lastModified: Date.now() });
  }
  // ✅ APNG 压缩(apng-js Player 合成 + UPNG 重编码)
  // 支持增量帧(blend/dispose),不再降级为静态压缩
  async function compressApngToSize(file, maxSizeKB = 2048, options = {}) {
    const maxBytes = maxSizeKB * 1024;
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null;
    if (file.size <= maxBytes) return file;
    const arrayBuffer = await file.arrayBuffer();
    // 用 apng-js 解析(它能正确处理所有 APNG 结构)
    // apng-js UMD 导出名为 "apng-js",需要从全局获取
    const _apngRaw = window['apng-js'] || window['apngJs'];
    const apngJsLib = typeof _apngRaw === 'function' ? _apngRaw
                    : (_apngRaw && typeof _apngRaw.default === 'function') ? _apngRaw.default
                    : null;
    if (!apngJsLib) {
      console.warn('[compressApngToSize] apng-js 库未加载 (window["apng-js"]=' + typeof _apngRaw + '),降级到静态压缩');
      toast('APNG 解析库未加载,将转为静态图片压缩(动画会丢失)', 3000);
      return compressImageToSize(file, maxSizeKB);
    }
    let apng;
    try {
      apng = apngJsLib(arrayBuffer);
    } catch (e) {
      console.warn('[compressApngToSize] parseAPNG 失败,降级到静态压缩:', e.message);
      toast('APNG 解码失败,将转为静态图片压缩(动画会丢失)', 0, { type: 'error' });
      return compressImageToSize(file, maxSizeKB);
    }
    if (apng instanceof Error) {
      console.warn('[compressApngToSize] parseAPNG 返回错误:', apng.message);
      toast('APNG 解码失败,将转为静态图片压缩(动画会丢失)', 0, { type: 'error' });
      return compressImageToSize(file, maxSizeKB);
    }
    const { width, height } = apng;
    if (!apng.frames || apng.frames.length <= 1) {
      return compressImageToSize(file, maxSizeKB);
    }
    console.log(`[compressApngToSize] APNG: ${width}x${height}, ${apng.frames.length}帧, 总时长=${apng.playTime}ms`);
    // ---- 第一步:用 apng-js Player 逐帧合成,拿到每帧的完整 RGBA 像素 ----
    await apng.createImages();
    const renderCanvas = document.createElement('canvas');
    renderCanvas.width = width;
    renderCanvas.height = height;
    const renderCtx = renderCanvas.getContext('2d', { willReadFrequently: true });
    const player = await apng.getPlayer(renderCtx, false);
    const compositedFrames = [];  // { data: Uint8ClampedArray, delay: number }
    for (let i = 0; i < apng.frames.length; i++) {
      await player.renderNextFrame();
      const imgData = renderCtx.getImageData(0, 0, width, height);
      compositedFrames.push({
        data: new Uint8Array(imgData.data),
        delay: apng.frames[i].delay || 100
      });
    }
    console.log(`[compressApngToSize] 合成完成: ${compositedFrames.length}帧`);
    // 验证合成帧数据完整性
    const expectedFrameBytes = width * height * 4;
    for (let i = 0; i < compositedFrames.length; i++) {
      if (compositedFrames[i].data.length !== expectedFrameBytes) {
        throw new Error(`合成帧${i}数据异常: 期望${expectedFrameBytes}字节, 实际${compositedFrames[i].data.length}字节`);
      }
    }
    console.log(`[compressApngToSize] 帧数据验证通过: ${compositedFrames.length}帧 × ${(expectedFrameBytes/1024).toFixed(0)}KB/帧`);
    // ---- 第二步:可选抽帧(帧数 > 15 且压缩压力大时启用)----
    let workFrames = compositedFrames;
    let workDelays = compositedFrames.map(f => f.delay);
    const originalFrameCount = compositedFrames.length;
    const oversizeRatio = file.size / maxBytes;
    if (oversizeRatio > 1.8 && compositedFrames.length > 15) {
      const step = oversizeRatio > 3 ? 3 : 2;
      const decimated = [];
      const decimatedDelays = [];
      for (let i = 0; i < compositedFrames.length; i += step) {
        decimated.push(compositedFrames[i]);
        let accumulatedDelay = 0;
        for (let j = i; j < Math.min(i + step, compositedFrames.length); j++) {
          accumulatedDelay += compositedFrames[j].delay;
        }
        decimatedDelays.push(accumulatedDelay);
      }
      workFrames = decimated;
      workDelays = decimatedDelays;
      console.log(`[compressApngToSize] 抽帧: ${originalFrameCount} → ${workFrames.length}帧 (step=${step})`);
    }
    // ---- 第三步:逐帧 canvas.toBlob + 手动组装 APNG ----
    // 完全绕过 UPNG.encode 的多帧模式,避免大数据量内存爆炸
    // PNG chunk 读取辅助
    const readPngChunks = (u8) => {
      const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
      let pos = 8; // 跳过 PNG signature
      const chunks = [];
      while (pos + 8 <= u8.length) {
        const len = dv.getUint32(pos); pos += 4;
        const type = String.fromCharCode(u8[pos], u8[pos+1], u8[pos+2], u8[pos+3]); pos += 4;
        if (pos + len + 4 > u8.length) break;
        chunks.push({ type, data: u8.slice(pos, pos + len) });
        pos += len + 4; // data + crc
      }
      return chunks;
    };
    // 手动组装 APNG
    const buildApng = (ihdrData, frameChunks, delays, numPlays) => {
      // 计算总大小
      // 第 1 帧: fcTL(38) + IDAT(12+len)*
      // 后续帧: fcTL(38) + fdAT(12+4+len)* (fdAT 多 4 字节 seqNum)
      let totalSize = 8 + (12 + 13) + (12 + 8); // sig + IHDR + acTL
      for (let i = 0; i < frameChunks.length; i++) {
        totalSize += 12 + 26; // fcTL
        for (const idat of frameChunks[i]) {
          totalSize += (i === 0 ? 12 : 12 + 4) + idat.length; // IDAT vs fdAT
        }
      }
      totalSize += 12; // IEND
      const buf = new ArrayBuffer(totalSize);
      const u8 = new Uint8Array(buf);
      const dv = new DataView(buf);
      let o = 0;
      // PNG signature
      u8.set([137, 80, 78, 71, 13, 10, 26, 10], 0); o = 8;
      // IHDR
      const ihdrLen = 13;
      dv.setUint32(o, ihdrLen); o += 4;
      u8.set([73, 72, 68, 82], o); o += 4; // "IHDR"
      u8.set(ihdrData, o); o += ihdrLen;
      // CRC32 of type+data
      const ihdrCrc = crc32(u8.slice(o - 4 - ihdrLen, o));
      dv.setUint32(o, ihdrCrc); o += 4;
      // acTL (animation control)
      dv.setUint32(o, 8); o += 4; // length
      u8.set([97, 99, 84, 76], o); o += 4; // "acTL"
      dv.setUint32(o, frameChunks.length); o += 4; // num_frames
      dv.setUint32(o, numPlays); o += 4; // num_plays (0 = infinite)
      const acTlCrc = crc32(u8.slice(o - 4 - 8, o));
      dv.setUint32(o, acTlCrc); o += 4;
      // Frames — APNG 规范:第 1 帧用 IDAT,后续帧用 fdAT
      let seqNum = 0;
      for (let i = 0; i < frameChunks.length; i++) {
        const w = dv.getUint32(16); // from IHDR
        const h = dv.getUint32(20);
        const delay = delays[i] || 100;
        // fcTL (frame control) — 26 bytes data
        dv.setUint32(o, 26); o += 4;
        u8.set([102, 99, 84, 76], o); o += 4; // "fcTL"
        dv.setUint32(o, seqNum++); o += 4; // sequence_number
        dv.setUint32(o, w); o += 4;
        dv.setUint32(o, h); o += 4;
        dv.setUint32(o, 0); o += 4; // x_offset
        dv.setUint32(o, 0); o += 4; // y_offset
        dv.setUint16(o, delay); o += 2; // delay_num
        dv.setUint16(o, 1000); o += 2; // delay_den (毫秒)
        // 全画布预合成帧:每帧都是完整 RGBA,必须 SOURCE 替换,不能 OVER 叠透明
        u8[o++] = 0; // dispose_op: NONE
        u8[o++] = 0; // blend_op: SOURCE(0=替换,保留本帧透明区)
        dv.setUint32(o, crc32(u8.slice(o - 4 - 26, o))); o += 4;
        if (i === 0) {
          // 第 1 帧:IDAT(标准 PNG 图像数据块,无额外 seqNum)
          for (const idatData of frameChunks[i]) {
            dv.setUint32(o, idatData.length); o += 4;
            u8.set([73, 68, 65, 84], o); o += 4; // "IDAT"
            u8.set(idatData, o); o += idatData.length;
            dv.setUint32(o, crc32(u8.slice(o - 4 - idatData.length, o))); o += 4;
          }
        } else {
          // 后续帧:fdAT(frame data,比 IDAT 多 4 字节 seqNum)
          for (const idatData of frameChunks[i]) {
            const fdatPayloadLen = 4 + idatData.length; // seqNum + 压缩数据
            dv.setUint32(o, fdatPayloadLen); o += 4;
            u8.set([102, 100, 65, 84], o); o += 4; // "fdAT"
            dv.setUint32(o, seqNum++); o += 4; // sequence_number
            u8.set(idatData, o); o += idatData.length;
            // CRC 覆盖 type("fdAT") + payload(seqNum + data)
            dv.setUint32(o, crc32(u8.slice(o - 4 - idatData.length - 4, o))); o += 4;
          }
        }
      }
      // IEND
      dv.setUint32(o, 0); o += 4;
      u8.set([73, 69, 78, 68], o); o += 4;
      dv.setUint32(o, crc32(u8.slice(o - 4, o))); o += 4;
      return buf;
    };
    // CRC32 查找表
    const crcTable = (() => {
      const table = new Uint32Array(256);
      for (let n = 0; n < 256; n++) {
        let c = n;
        for (let k = 0; k < 8; k++) {
          c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
        }
        table[n] = c;
      }
      return table;
    })();
    const crc32 = (data) => {
      let crc = 0xFFFFFFFF;
      for (let i = 0; i < data.length; i++) {
        crc = crcTable[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
      }
      return (crc ^ 0xFFFFFFFF) >>> 0;
    };
    // 逐帧渲染到 canvas + toBlob
    const renderFrameToBlob = (srcData, srcW, srcH, scale) => {
      return new Promise((resolve, reject) => {
        const newW = Math.max(1, Math.floor(srcW * scale));
        const newH = Math.max(1, Math.floor(srcH * scale));
        const canvas = document.createElement('canvas');
        canvas.width = newW;
        canvas.height = newH;
        const ctx = canvas.getContext('2d');
        ctx.imageSmoothingEnabled = true;
        ctx.imageSmoothingQuality = 'high';
        // 通过 Image → canvas 方式绘制(避免 OffscreenCanvas + putImageData 的内存问题)
        const img = new Image();
        const blob = new Blob([srcData], { type: 'image/bmp' });
        const url = URL.createObjectURL(blob);
        img.onload = () => {
          ctx.drawImage(img, 0, 0, newW, newH);
          URL.revokeObjectURL(url);
          canvas.toBlob((pngBlob) => {
            if (!pngBlob) { reject(new Error('toBlob 返回 null')); return; }
            pngBlob.arrayBuffer().then(ab => resolve(new Uint8Array(ab))).catch(reject);
          }, 'image/png');
        };
        img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Image 加载失败')); };
        img.src = url;
      });
    };
    // 单帧编码:canvas 缩放 → toBlob('image/png')
    // 在右下角放一个几乎透明的像素,强制浏览器输出 RGBA(type=6),避免偷换成 RGB
    const renderFrameToPng = (rgbaData, srcW, srcH, scale) => {
      return new Promise((resolve, reject) => {
        const newW = Math.max(1, Math.floor(srcW * scale));
        const newH = Math.max(1, Math.floor(srcH * scale));
        const srcCanvas = document.createElement('canvas');
        srcCanvas.width = srcW;
        srcCanvas.height = srcH;
        const srcCtx = srcCanvas.getContext('2d');
        srcCtx.putImageData(new ImageData(new Uint8ClampedArray(rgbaData), srcW, srcH), 0, 0);
        const dstCanvas = document.createElement('canvas');
        dstCanvas.width = newW;
        dstCanvas.height = newH;
        const dstCtx = dstCanvas.getContext('2d');
        dstCtx.imageSmoothingEnabled = true;
        dstCtx.imageSmoothingQuality = 'high';
        dstCtx.drawImage(srcCanvas, 0, 0, newW, newH);
        // 强制 RGBA:在不可见角落放一个几乎全透明的像素
        dstCtx.fillStyle = 'rgba(0,0,0,0.004)';
        dstCtx.fillRect(newW - 1, newH - 1, 1, 1);
        dstCanvas.toBlob((pngBlob) => {
          if (!pngBlob) { reject(new Error('toBlob 返回 null')); return; }
          pngBlob.arrayBuffer().then(ab => resolve(new Uint8Array(ab))).catch(reject);
        }, 'image/png');
      });
    };
    const targetLowerBytes = Math.floor((maxSizeKB - 68) * 1024);
    const startTime = performance.now();
    // 缩放梯度:从大到小(直接枚举,避免浮点死循环);超限时按实测体积跳档
    const scaleSteps = [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.15];
    for (let stepIndex = 0; stepIndex < scaleSteps.length; stepIndex++) {
      const scale = scaleSteps[stepIndex];
      const newW = Math.max(1, Math.floor(width * scale));
      const newH = Math.max(1, Math.floor(height * scale));
      console.log(`[compressApngToSize] 尝试 scale=${scale.toFixed(2)}: ${newW}x${newH}, ${workFrames.length}帧`);
      try {
        // 逐帧渲染为 PNG
        const framePngs = [];
        for (let i = 0; i < workFrames.length; i++) {
          const pngU8 = await renderFrameToPng(workFrames[i].data, width, height, scale);
          framePngs.push(pngU8);
        }
        // 解析第一帧的 IHDR
        const firstChunks = readPngChunks(framePngs[0]);
        const ihdrChunk = firstChunks.find(c => c.type === 'IHDR');
        if (!ihdrChunk) throw new Error('第一帧 PNG 缺少 IHDR');
        // 提取每帧的 IDAT 数据
        const allFrameIdats = framePngs.map(png => {
          const chunks = readPngChunks(png);
          return chunks.filter(c => c.type === 'IDAT').map(c => c.data);
        });
        // 组装 APNG
        const apngBuf = buildApng(ihdrChunk.data, allFrameIdats, workDelays, 0);
        const blob = new Blob([apngBuf], { type: 'image/apng' });
        const sizeKB = blob.size / 1024;
        console.log(`[compressApngToSize] scale=${scale.toFixed(2)} → ${sizeKB.toFixed(1)}KB`);
        if (onProgress) {
          onProgress({ scale, sizeKB, originalKB: file.size / 1024, frameCount: workFrames.length, newW: Math.floor(width * scale), newH: Math.floor(height * scale) });
        }
        if (blob.size >= targetLowerBytes && blob.size <= maxBytes) {
          const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
          console.log(`[compressApngToSize] ✓ 命中目标: ${sizeKB.toFixed(1)}KB, ${elapsed}秒`);
          return new File([blob], file.name.replace(/\.\w+$/i, '') + '-compressed.png', { type: 'image/png' });
        }
        if (blob.size <= maxBytes) {
          // 低于上限但未命中下限——也接受
          const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
          console.log(`[compressApngToSize] ✓ 完成: ${(file.size/1024).toFixed(1)}KB → ${sizeKB.toFixed(1)}KB, ${elapsed}秒`);
          return new File([blob], file.name.replace(/\.\w+$/i, '') + '-compressed.png', { type: 'image/png' });
        }
        // 仍超限:按 scale² 预估下一档,跳过中间无效梯度
        if (stepIndex < scaleSteps.length - 1) {
          const predicted = scale * Math.sqrt(maxBytes / Math.max(blob.size, 1)) * 0.92;
          let jumpTo = stepIndex + 1;
          for (let j = stepIndex + 1; j < scaleSteps.length; j++) {
            jumpTo = j;
            if (scaleSteps[j] <= predicted + 1e-6) break;
          }
          if (jumpTo > stepIndex + 1) {
            console.log(`[compressApngToSize] 跳档: ${scale.toFixed(2)} (${sizeKB.toFixed(1)}KB) 预估=${predicted.toFixed(3)} → ${scaleSteps[jumpTo].toFixed(2)}`);
            stepIndex = jumpTo - 1;
          }
        }
      } catch (e) {
        console.error(`[compressApngToSize] scale=${scale.toFixed(2)} 失败:`, e.message);
        if (onProgress) {
          onProgress({ scale, error: e.message, originalKB: file.size / 1024 });
        }
      }
    }
    throw new Error('APNG 压缩失败:所有缩放比例均无法降到限制以内,请手动处理');
  }
  function interceptReplyForm() {
    // —— 缓存工具(GM_* 优先;localStorage 兜底;返回对象型默认值) ——
    function cacheGet(key, fallback = null) {
      try {
        if (typeof GM_getValue === 'function') {
          const v = GM_getValue(key);
          return v != null ? v : fallback;
        }
        const s = localStorage.getItem(key);
        return s != null ? JSON.parse(s) : fallback;
      } catch (_) {
        return fallback;
      }
    }
    function cacheSet(key, val) {
      try {
        if (typeof GM_setValue === 'function') {
          GM_setValue(key, val);
          return;
        }
        localStorage.setItem(key, JSON.stringify(val));
      } catch (_) {}
    }
    // —— NFKC 候选池 ——
    function getNkfcBase() {
      const CACHE_KEY_NKFC = 'nkfcBase.v2';
      let base = cacheGet(CACHE_KEY_NKFC, {});
      if (Object.keys(base).length > 0) return base;
      base = {};
      for (let i = 0; i <= 0xFFFF; i++) {
        const ch = String.fromCharCode(i);
        const norm = ch.normalize('NFKC');
        if (ch !== norm) (base[norm] ||= []).push(ch);
      }
      cacheSet(CACHE_KEY_NKFC, base);
      return base;
    }
    // —— 单字符替换 ——
    // —— 颜文字中常见的汉字与英文,替换时排除(可保留或按需维护) ——
    const KAOMOJI_EXCLUDE_CHARS = new Set([
      '旦','开','摆','摔','低','好','钩','我','咬','接','龙','大','成','功',
      '举','高','糕','咩','吁','肥','喵','酱','狗','比','汪','哈','電','柱',
      'N','o','O','o'
    ]);
    // —— 单字符替换(回归稳定候选:优先第一个),但保留缓存为数组以便未来扩展 ——
    function maskChar(ch, skipAscii = true) {
      const CACHE_KEY_CHARMAP = 'unvcodeCharMap.v3';
      const charMap = cacheGet(CACHE_KEY_CHARMAP, {});
      if (charMap.hasOwnProperty(ch)) {
        const cached = charMap[ch];
        if (Array.isArray(cached) && cached.length > 0) {
          return cached[0]; // 稳定使用首选候选,旧版风格
        } else if (typeof cached === 'string') {
          return cached;
        }
      }
      if (skipAscii && ch.charCodeAt(0) < 128) {
        charMap[ch] = [ch];
        cacheSet(CACHE_KEY_CHARMAP, charMap);
        return ch;
      }
      const base = getNkfcBase();
      const norm = ch.normalize('NFKC');
      const candidates = base[norm] || [];
      const mapped = candidates.length > 0 ? candidates[0] : ch;
      charMap[ch] = [mapped];
      cacheSet(CACHE_KEY_CHARMAP, charMap);
      return mapped;
    }
    // —— 逐字替换(委托 maskChar) ——
    function unvcode(text, skipAscii = true) {
      let out = '';
      for (const ch of text) out += maskChar(ch, skipAscii);
      return out;
    }
    // —— 保留你之前的选择性规则:URL 整段跳过;非 URL 部分中文 → unvcode;英文 → 原样 + U+200B;其他原样 ——
    function unvcodeSelective(text) {
      const urlRegex = /(?:(?:https?|ftp):\/\/[^\s]+|www\.[^\s]+|[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s]*)?|>>(?:No\.)?\d+)/gi;
      const hanRegex = /[\u4E00-\u9FFF]/;
      const engRegex = /[A-Za-z]/;
      return text
        .split(/(\b(?:https?|ftp):\/\/[^\s]+|www\.[^\s]+|[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s]*)?|>>(?:No\.)?\d+)/gi)
        .map(part => {
          if (!part) return '';
          // URL 整段跳过
          if (urlRegex.test(part)) {
            urlRegex.lastIndex = 0;
            return part;
          }
          // 非 URL:按规则处理
          let out = '';
          for (const ch of part) {
            if (hanRegex.test(ch) && !KAOMOJI_EXCLUDE_CHARS.has(ch)) {
              out += unvcode(ch, false); // 中文 → unvcode
            } else if (engRegex.test(ch) && !KAOMOJI_EXCLUDE_CHARS.has(ch)) {
              out += ch + '\u200B';     // 英文 → 原样 + U+200B
            } else {
              out += ch;                // 其他 → 原样
            }
          }
          return out;
        })
        .join('');
    }
    // —— 根据当前失败文本,精准刷新需处理字符的缓存 ——
    // 仅刷新:非 URL 段中的中文与英文,且不在排除集合中的字符
    function resetCacheForFailedContent(text) {
      const urlRegex = /(?:(?:https?|ftp):\/\/[^\s]+|www\.[^\s]+|[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s]*)?|>>(?:No\.)?\d+)/gi;
      const hanRegex = /[\u4E00-\u9FFF]/;
      const engRegex = /[A-Za-z]/;
      const CACHE_KEY_CHARMAP = 'unvcodeCharMap.v3';
      const charMap = cacheGet(CACHE_KEY_CHARMAP, {});
      const parts = text.split(/(\b(?:https?|ftp):\/\/[^\s]+|www\.[^\s]+|[a-z0-9.-]+\.[a-z]{2,}(?:\/[^\s]*)?|>>(?:No\.)?\d+)/gi);
      let changed = false;
      for (const part of parts) {
        if (!part) continue;
        // 跳过 URL 段
        if (urlRegex.test(part)) {
          urlRegex.lastIndex = 0;
          continue;
        }
        // 非 URL 段:逐字检查
        for (const ch of part) {
          const isHan = /[\u4E00-\u9FFF]/.test(ch);
          const isEng = /[A-Za-z]/.test(ch);
          if (!isHan && !isEng) continue;
          if (KAOMOJI_EXCLUDE_CHARS.has(ch)) continue;
          // 清空该字符的缓存(使下次重新生成替换)
          if (charMap[ch]) {
            delete charMap[ch];
            changed = true;
          }
        }
      }
      if (changed) cacheSet(CACHE_KEY_CHARMAP, charMap);
    }
    // done 可选unvcode模式或者零宽空格模式,目前来看unvcode模式下长文本中被替换的文字较多,观感受影响,只要没有复制需求,零宽空格更实用
    // done BUG 其他报错似乎不toast提示-大概是好了吧,新增了500报错的toast,但目前图床有问题没法确定图片安全性审核不通过是不是也会toast
    // —— 表单提交拦截 ——
    document.addEventListener('submit', function (e) {
      const form = e.target;
      const isReply = form.matches('form[action="/Home/Forum/doReplyThread.html"]');
      const isPost = form.matches('form[action="/Home/Forum/doPostThread.html"]');
      if (!isReply && !isPost) return;
      e.preventDefault();
      const formData = new FormData(form);
      // 文字内容
      let content = (formData.get('content') || '').toString().trim();
      // 新增:如果内容只有 "0",就在后面加一个零宽空格
      if (content === '0') {
        content = '0\u200B'; // 在 0 后追加零宽空格
        formData.set('content', content);
        const textarea = form.querySelector('textarea[name="content"]');
        if (textarea) textarea.value = content;
      }
      if (!content) {
        // 检查是否选择了图片(支持 name="image")
        const fileInput = form.querySelector('input[type="file"][name="image"]');
        const hasImage = !!(fileInput && fileInput.files && fileInput.files.length > 0);
        if (hasImage || content === '0') {
          //无文字但有图片,或者内容只有 "0" → 自动补零宽空格,并同步到 FormData 与 DOM
          //修改以自定义。
          //content = '分享图片';//默认占位文字
          //content = '‎';//空格占位符 U+200E
          //content = ' ';//全角空格占位符 U+3000
          content = '​'; // 零宽空格占位符 U+200B
          formData.set('content', content);
          const textarea = form.querySelector('textarea[name="content"]');
          if (textarea) textarea.value = content;
        } else {
          toast(isReply ? '回复内容不能为空' : '发串内容不能为空');
          return;
        }
      }
      // 检查错误信息是否与图片大小有关
      function isImageSizeError(msg) {
        if (!msg) return false;
        const patterns = [
          /图片.*大/i,
          /不能超过.*2048/i,
          /超过.*2048/i,
          /2048.*kb/i,
          /文件.*大/i,
          /image.*size/i,
          /file.*large/i,
          /too\s*large/i
        ];
        return patterns.some(p => p.test(msg));
      }
      function resetIllegalRetryState(options = {}) {
        const { clearOriginalContent = false } = options;
        form.__illegalRetryCount = 0;
        form.__illegalRetryCountU200B = 0;
        if (clearOriginalContent) {
          form.__originalContent = null;
        }
      }
      function processTextPreservingHiddenTags(input, processor) {
        if (!input || typeof processor !== 'function') return input || '';
        return input
          .split(/(\[h\]|\[\/h\])/gi)
          .map(part => (/^\[h\]$|^\[\/h\]$/i.test(part) ? part : processor(part)))
          .join('');
      }
      function classifySubmitResponse(doc, html) {
        const successMsg = doc.querySelector('p.success');
        const errorMsg = doc.querySelector('p.error');
        if (successMsg) return { kind: 'success', node: successMsg };
        if (errorMsg) return { kind: 'error', node: errorMsg };
        try {
          if (/500\s+Internal\s+Server\s+Error/i.test(html) || html.indexOf('<title>500 Internal Server Error</title>') !== -1) {
            return { kind: 'fallback-error', message: '500 Internal Server Error,可能是图床故障' };
          }
          const errorDiv = doc.querySelector('div.error');
          const h1 = errorDiv && errorDiv.querySelector('h1');
          if (h1 && h1.textContent.trim()) {
            return { kind: 'fallback-error', message: h1.textContent.trim() };
          }
          const titleEl = doc.querySelector('title');
          if (titleEl && /系统发生错误|系统错误/i.test(titleEl.textContent)) {
            return { kind: 'fallback-error', message: '系统发生错误' };
          }
        } catch (e) {
          console.warn('[interceptReplyForm] error while checking fallback formats:', e);
        }
        return { kind: 'unknown' };
      }
      function classifySubmitError(msg) {
        if (isImageSizeError(msg)) return 'image-size';
        if (/格式不正确/.test(msg)) return 'image-format';
        if (/非法图像/.test(msg)) return 'illegal-image';
        if (/含有非法词语/.test(msg)) return 'illegal-word';
        return 'other';
      }
      function isSubmitNetworkError(err) {
        // fetch 抛 TypeError = 网络层失败(断网/连接重置/CORS 等),不是超时;
        // 真正的超时由 doSubmit 的 AbortController 处理(err.name === 'AbortError')。
        // 请求可能已到达服务器(如响应途中断连),因此调用方按「结果未知」处理。
        return !!(err && err.name === 'TypeError' && /fetch|network|load failed/i.test(err.message || ''));
      }
      function formatSubmitError(err) {
        return '发送失败:' + ((err && err.message) || '未知错误');
      }
      function clearSuccessfulSubmitForm() {
        const textarea = form.querySelector('textarea[name="content"]');
        if (textarea) { textarea.value = ''; textarea.dispatchEvent(new Event('input', { bubbles: true })); }
        const fileInput = form.querySelector('input[type="file"][name="image"]');
        if (fileInput) {
          fileInput.value = '';
          // 附件随发送清空:同步复位 HEIC 预览去重状态,避免下一条同文件不再提示
          if (typeof updatePreviewImageFromFile === 'function') updatePreviewImageFromFile(null);
        }
        form.__preparedImage = null;
        deleteDraftSafe(getDraftKey());
        const titleInput = form.querySelector('input[name="title"]');
        if (titleInput) { titleInput.value = ''; titleInput.dispatchEvent(new Event('input', { bubbles: true })); }
        const nameInput = form.querySelector('input[name="name"]');
        if (nameInput) { nameInput.value = ''; nameInput.dispatchEvent(new Event('input', { bubbles: true })); }
        const emailInput = form.querySelector('input[name="email"]');
        if (emailInput) { emailInput.value = ''; emailInput.dispatchEvent(new Event('input', { bubbles: true })); }
        document.dispatchEvent(new CustomEvent('replySuccess', {
          detail: { key: getDraftKey(), tid: form.querySelector('input[name="resto"]')?.value || '' }
        }));
      }
      function runSuccessfulSubmitNavigation(confirmPromise) {
        if (isReply) {
          // 板块页/时间线页:跳过全量刷新,由 handleBoardQuickReplyRefresh 做增量更新
          // 串内页:正常全量刷新
          const _isBoardOrTimeline = PageType.isBoardOrTimelinePage();
          if (!_isBoardOrTimeline) {
            try {
              // 错峰 150ms 发起:避开发送后发言历史回查(getLastPost/ref/thread api)的并发峰值
              setTimeout(() => {
                refreshRepliesWithSeamlessPaging((result) => {
                  const ok = !(result && result.ok === false);
                  // 刷新完成(翻页逻辑已在内部处理)
                  recordCurrentThreadHistory(0, { reason: 'reply-success-refresh', countVisit: false, touchVisitedAt: true });
                  if (ok) {
                    console.log('回复区刷新完成');
                  } else {
                    console.warn('回复区刷新失败', result && result.reason);
                  }
                }, { getConfig: safeGetConfig });
              }, 150);
            } catch (err) {
              console.error('refreshRepliesWithSeamlessPaging 调用失败', err);
            }
          }
          return;
        }
        // 发串:根据设置决定行为
        const cfg = (typeof SettingPanel !== 'undefined' && SettingPanel.state) ? SettingPanel.state : {};
        const postAction = cfg.postAfterAction === 'refresh' ? 'refresh' : 'jump';
        if (postAction === 'jump' && confirmPromise) {
          toast('新串发送成功,正在确认地址……');
          Promise.race([
            confirmPromise,
            new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), POST_HISTORY_CONFIRM_TIMEOUT_MS))
          ]).then(confirmed => {
            if (confirmed && confirmed.url) {
              window.open(confirmed.url, '_blank');
              toast('已在新标签页打开新串');
            } else {
              toast('新串已发送,但未能确认地址', 0, { type: 'error' });
            }
          }).catch(() => {
            toast('新串已发送,确认地址超时', 0, { type: 'error' });
          });
        } else if (postAction === 'refresh' && confirmPromise) {
          // refresh 模式:等待发言历史确认后,跳转板块第一页顶部
          toast('新串发布成功,正在确认地址……');
          Promise.race([
            confirmPromise,
            new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), POST_HISTORY_CONFIRM_TIMEOUT_MS))
          ]).then(() => {
            const boardPage1 = location.origin + location.pathname + '?page=1';
            toast('正在转版块第一页……');
            window.location.href = boardPage1;
          }).catch(() => {
            const boardPage1 = location.origin + location.pathname + '?page=1';
            toast('确认地址超时,正在转版块第一页……');
            window.location.href = boardPage1;
          });
        } else {
          // jump 模式但无 confirmPromise:直接刷新页面
          location.reload();
        }
      }
      // 发送前处理状态:复用「发送中」状态 toast 节点(key=send-status)。
      // 处理期间长驻留并原地更新,完成/失败时替换或清除,避免用户误以为提交卡住。
      function showSubmitPreflightStatus(msg, duration = 600000) {
        try {
          toast(msg, duration, { queue: false, key: 'send-status' });
        } catch (e) {
          console.warn('[interceptReplyForm] 发送前状态提示失败:', e);
        }
      }
      function clearSubmitPreflightStatus() {
        try {
          const $t = $('#xdex-immediate-toast-send-status');
          if ($t.length) {
            $t.stop(true, false);
            $t.fadeOut(120, () => $t.remove());
          }
        } catch (e) { /* 忽略清理失败 */ }
      }
      // 服务端拒绝后的本地重试处理(转换/压缩/GIF 修复)统一 wall-clock 上限。
      // 超时后调用方的 await 立即 reject;迟到的结果不会被使用,内部 notify 需自检 token。
      function runImageProcessingWithTimeout(label, operation, timeoutHint = '请更换图片,或手动压缩/转换后重试。') {
        const token = { cancelled: false };
        let timer = 0;
        const seconds = Math.round(SUBMIT_IMAGE_PROCESSING_TIMEOUT_MS / 1000);
        const timeoutPromise = new Promise((_, reject) => {
          timer = setTimeout(() => {
            token.cancelled = true;
            const err = new Error(`${label}超时(${seconds} 秒),本次未发送。${timeoutHint}`);
            err.__imageProcessingTimeout = true;
            reject(err);
          }, SUBMIT_IMAGE_PROCESSING_TIMEOUT_MS);
        });
        return Promise.race([
          Promise.resolve().then(() => operation(token)),
          timeoutPromise
        ]).finally(() => clearTimeout(timer));
      }
      async function compressImageForRetry(file, actualFormat, notify = toast) {
        if (actualFormat === 'gif') {
          let gifFile = file;
          if (!/\.gif$/i.test(file.name)) {
            const fixedName = file.name.replace(/\.\w+$/i, '.gif');
            gifFile = new File([file], fixedName, { type: 'image/gif', lastModified: file.lastModified });
            console.log(`[interceptReplyForm] 后缀已修正: ${file.name} → ${fixedName}`);
          }
          const gifResult = await compressGifToSize(gifFile, {
            maxSizeKB: 2048,
            targetLowerKB: 1850,
            acceptableLowerKB: 1780,
            onProgress: (progress) => {
              if (progress.error) {
                notify(`GIF压缩中:第${progress.index}/${progress.total}次失败,继续尝试……`, 1800);
                return;
              }
              notify(`GIF压缩中:第${progress.index}/${progress.total}次,${progress.originalKB.toFixed(1)}KB → ${progress.currentKB.toFixed(1)}KB`, 1800);
            }
          });
          console.log(`[interceptReplyForm] GIF压缩完成: ${gifResult.summary}`);
          const compressedFile = gifResult.file;
          try {
            Object.defineProperty(compressedFile, '__xdexGifCompressionDurationMs', {
              value: gifResult.durationMs,
              enumerable: false,
              configurable: true
            });
          } catch (metadataError) {
            console.warn('[X岛-EX][GIF压缩] 无法附加耗时 metadata:', metadataError);
          }
          return compressedFile;
        }
        if (actualFormat === 'apng') {
          const compressedFile = await compressApngToSize(file, 2048, {
            onProgress: (progress) => {
              if (progress.error) {
                notify(`APNG压缩中:scale=${progress.scale.toFixed(2)} 失败,继续尝试……`, 1800);
                return;
              }
              notify(`APNG压缩中:${progress.newW}x${progress.newH} ${progress.frameCount}帧 → ${progress.sizeKB.toFixed(0)}KB(原${progress.originalKB.toFixed(0)}KB)`, 1800);
            }
          });
          console.log(`[interceptReplyForm] APNG压缩完成: ${(compressedFile.size / 1024).toFixed(1)}KB`);
          return compressedFile;
        }
        if (actualFormat === 'animated-webp') {
          notify('检测到动画 WebP,正在转换为 APNG 保留动画……', 2000);
          // 先转 APNG(保留动画),再按需压缩
          let convertedFile = await convertWebpToAcceptedFormat(file, true);
          console.log(`[interceptReplyForm] 动画WebP→APNG: ${(file.size / 1024).toFixed(1)}KB -> ${(convertedFile.size / 1024).toFixed(1)}KB`);
          if (convertedFile.size > SUBMIT_IMAGE_LIMIT_BYTES) {
            console.log(`[interceptReplyForm] 转换后 ${(convertedFile.size / 1024).toFixed(1)}KB 仍超限,用 APNG 压缩`);
            notify(`转换后 ${(convertedFile.size / 1024).toFixed(1)}KB 仍超限,正在压缩动画……`, 0, { type: 'error' });
            const compressResult = await compressApngToSize(convertedFile, 2048, {
              onProgress: (progress) => {
                if (progress.error) {
                  notify(`APNG压缩中:scale=${progress.scale.toFixed(2)} 失败,继续尝试……`, 1800);
                  return;
                }
                notify(`APNG压缩中:${progress.newW}x${progress.newH} ${progress.frameCount}帧 → ${progress.sizeKB.toFixed(0)}KB(原${progress.originalKB.toFixed(0)}KB)`, 1800);
              }
            });
            convertedFile = compressResult.file || compressResult;
            console.log(`[interceptReplyForm] APNG压缩完成: ${(convertedFile.size / 1024).toFixed(1)}KB`);
          }
          return convertedFile;
        }
        if (actualFormat === 'heic') {
          notify('检测到 HEIC,正在转换为 PNG……', 2000);
          let convertedFile = await convertHeicToPng(file);
          console.log(`[interceptReplyForm] HEIC→PNG: ${(file.size / 1024).toFixed(1)}KB -> ${(convertedFile.size / 1024).toFixed(1)}KB`);
          if (convertedFile.size > SUBMIT_IMAGE_LIMIT_BYTES) {
            console.log(`[interceptReplyForm] 转换后 ${(convertedFile.size / 1024).toFixed(1)}KB 仍超限,继续压缩`);
            convertedFile = await compressImageToSize(convertedFile, 2048, 0.6, 'png');
            console.log(`[interceptReplyForm] 压缩完成: ${(convertedFile.size / 1024).toFixed(1)}KB`);
          }
          return convertedFile;
        }
        const compressedFile = await compressImageToSize(file, 2048, 0.6, actualFormat);
        console.log(`[interceptReplyForm] 压缩后大小: ${(compressedFile.size / 1024).toFixed(1)}KB`);
        console.log(`[interceptReplyForm] 静态图片压缩完成: ${(compressedFile.size / 1024).toFixed(1)}KB`);
        return compressedFile;
      }
      // 发送前预检:只在真正要发请求时执行,避免选图即压缩。
      // 返回 { fd, prepared };prepared=true 表示图片已在本地处理过,后续错误分支不再重复压缩。
      function rememberPreparedSubmitImage(sourceFile, preparedFile) {
        if (!sourceFile || !preparedFile) return;
        form.__preparedImage = {
          name: sourceFile.name,
          size: sourceFile.size,
          lastModified: sourceFile.lastModified,
          file: preparedFile
        };
      }
      async function prepareImageBeforeSubmit(fd, token) {
        const isCancelled = () => !!(token && token.cancelled);
        // 超时后不再更新状态 toast,避免覆盖用户重试后的新状态
        const status = (msg, duration) => { if (!isCancelled()) showSubmitPreflightStatus(msg, duration); };
        const fileInput = form.querySelector('input[type="file"][name="image"]');
        const file = fileInput && fileInput.files && fileInput.files[0];
        if (!file) return { fd, prepared: false };
        if (!form.__preparedImageChangeBound) {
          form.__preparedImageChangeBound = true;
          fileInput.addEventListener('change', () => { form.__preparedImage = null; });
        }
        // 同一张图已处理过(例如非法词重试或用户再次点击发送)直接复用
        const cached = form.__preparedImage;
        if (cached && cached.file &&
            cached.name === file.name &&
            cached.size === file.size &&
            cached.lastModified === file.lastModified) {
          return { fd: cloneFormDataWithImage(fd, cached.file), prepared: true };
        }
        const actualFormat = await detectImageFormat(file);
        // 检测阶段也可能被超时取消;此时不得重新上锁或开始压缩
        if (isCancelled()) return { fd, prepared: false, cancelled: true };
        const mustConvert = actualFormat === 'webp' || actualFormat === 'animated-webp' || actualFormat === 'heic';
        const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
        const tooLarge = file.size > SUBMIT_IMAGE_LIMIT_BYTES;
        // 格式必须转换;体积则尊重「自动压缩图片」开关
        if (!mustConvert && !(tooLarge && cfg.interceptReplyFormAutoCompress)) {
          return { fd, prepared: false };
        }
        const sizeKB = (file.size / 1024).toFixed(1);
        const formatLabel = actualFormat === 'gif' ? ' GIF' : actualFormat === 'apng' ? ' APNG' : '';
        let startMsg;
        if (actualFormat === 'webp') {
          startMsg = `发送前处理:服务器不接受 WebP,正在转换为 PNG(${sizeKB}KB)……`;
        } else if (actualFormat === 'animated-webp') {
          startMsg = `发送前处理:服务器不接受动画 WebP,正在转换为 APNG(${sizeKB}KB)……`;
        } else if (actualFormat === 'heic') {
          startMsg = `发送前处理:服务器不接受 HEIC,正在转换为 PNG(${sizeKB}KB)……`;
        } else {
          startMsg = `发送前处理:图片 ${sizeKB}KB 超过 2048KB,正在压缩${formatLabel}……`;
        }
        status(startMsg);
        beginSubmitImageProcessing(form);
        try {
          // 压缩/转换过程中的进度统一写入同一状态 toast,避免长任务期间提示消失
          const notify = (msg) => status(`发送前处理 · ${msg}`);
          let outFile;
          if (actualFormat === 'webp') {
            // 静态 WebP:compressImageForRetry 会输出 WebP,服务器仍拒绝,必须先转 PNG
            outFile = await convertWebpToAcceptedFormat(file, false);
            if (outFile.size > SUBMIT_IMAGE_LIMIT_BYTES) {
              status(`发送前处理:转换后 ${(outFile.size / 1024).toFixed(1)}KB 仍超限,正在压缩……`);
              outFile = await compressImageForRetry(outFile, 'png', notify);
            }
          } else if (mustConvert) {
            // HEIC / 动画 WebP:compressImageForRetry 内含转换与按需压缩
            outFile = await compressImageForRetry(file, actualFormat, notify);
          } else {
            outFile = await compressImageForRetry(file, actualFormat, notify);
          }
          // 超时后迟到的处理结果:直接丢弃,不写缓存、不发送、不动表单状态
          if (isCancelled()) return { fd, prepared: false, cancelled: true };
          // 处理期间用户可能换了图:若输入框已不是同一张图,放弃本次处理结果,
          // 避免「界面显示新图、实际发送旧图」;缓存也不写入。
          const latestInput = form.querySelector('input[type="file"][name="image"]');
          const latestFile = latestInput && latestInput.files && latestInput.files[0];
          if (!latestFile ||
              latestFile.name !== file.name ||
              latestFile.size !== file.size ||
              latestFile.lastModified !== file.lastModified) {
            throw new Error('发送前处理期间图片已被更换或移除,本次未发送,请重新点击发送');
          }
          rememberPreparedSubmitImage(file, outFile);
          const gifMs = Number(outFile && outFile.__xdexGifCompressionDurationMs);
          const gifSeconds = Number.isFinite(gifMs) && gifMs >= 0 ? gifMs / 1000 : null;
          status(`发送前处理完成:已处理至 ${(outFile.size / 1024).toFixed(1)}KB${gifSeconds == null ? '' : `,耗时 ${gifSeconds.toFixed(2)} 秒`},正在提交……`, 2000);
          return { fd: cloneFormDataWithImage(fd, outFile), prepared: true };
        } finally {
          // 已超时取消时不要清掉可能属于新一轮预检的 processing 标记
          if (!isCancelled()) finishSubmitImageProcessing(form);
        }
      }
      async function doSubmit(fd, isRetry = false, prepared = false) {
        // 图片处理完成后递归提交时,在保持原提交锁的同时重新开始网络看门狗与真实超时。
        if (!isRetry && !prepared) {
          const token = { cancelled: false };
          let preflightTimer = 0;
          const timeoutPromise = new Promise((_, reject) => {
            preflightTimer = setTimeout(() => {
              token.cancelled = true;
              const timeoutErr = new Error(SUBMIT_IMAGE_PROCESSING_TIMEOUT_MSG);
              timeoutErr.__preflightTimeout = true;
              reject(timeoutErr);
            }, SUBMIT_IMAGE_PROCESSING_TIMEOUT_MS);
          });
          try {
            const prep = await Promise.race([
              prepareImageBeforeSubmit(fd, token),
              timeoutPromise
            ]);
            if (prep && prep.cancelled) return;
            fd = prep.fd;
            prepared = prep.prepared;
          } catch (prepErr) {
            clearSubmitPreflightStatus();
            if (prepErr && prepErr.__preflightTimeout) {
              // 预检超时:解锁由调用方 finally 负责;迟到的处理结果由 token 丢弃
              finishSubmitImageProcessing(form);
              toast(prepErr.message, 0, { type: 'error', key: 'send-error' });
              return;
            }
            console.error('[interceptReplyForm] 图片预检失败:', prepErr);
            toast(prepErr && prepErr.message ? prepErr.message : '图片预处理失败,请手动处理后重试', 0, { type: 'error' });
            return;
          } finally {
            clearTimeout(preflightTimer);
          }
        }
        // 新请求即将真正发出:旧的「结果未知」由本次结果覆盖
        // (放在预检之后,避免预检失败时丢失上一次超时的重复提交警告)
        form.__submitUnknown = false;
        finishSubmitImageProcessing(form);
        refreshSubmitLockTimer(form);
        const timeoutMs = computeSubmitRequestTimeoutMs(fd);
        let controller = null;
        let abortTimer = 0;
        if (typeof AbortController === 'function') {
          controller = new AbortController();
          form.__submitController = controller;
          abortTimer = setTimeout(() => {
            // 期间若已发起新的递归请求,旧计时器不得误杀新请求
            if (form.__submitController !== controller) return;
            form.__submitAbortTimer = 0;
            form.__submitUnknown = true;
            try { controller.abort('timeout'); } catch (e) { /* 老内核不支持 reason */ }
          }, timeoutMs);
          form.__submitAbortTimer = abortTimer;
        }
        // 用 Promise 包一层:fetch 同步抛错时也能进入 .catch,避免状态提示滞留
        return Promise.resolve().then(() => fetch(form.action, {
          method: form.method,
          body: fd,
          credentials: 'include',
          signal: controller ? controller.signal : undefined
        }))
        .then(res => res.text())
        .then(async html => {
          // 收到服务器响应 = 结果已确定(成功或业务错误):
          // 撤销本次请求的看门狗与中止计时器,避免后续图片转换/递归重试期间旧计时器误报。
          clearSubmitLockTimer(form);
          clearTimeout(abortTimer);
          if (form.__submitAbortTimer === abortTimer) form.__submitAbortTimer = 0;
          if (form.__submitController === controller) form.__submitController = null;
          form.__submitUnknown = false;
          // 响应已到达:结束「发送前处理/正在提交」状态,交给成功或错误提示
          clearSubmitPreflightStatus();
          // 增加调试输出:打印响应 HTML 的前 2000 字符,避免控制台被大量内容淹没
          try { console.log('[interceptReplyForm] response html (truncated):', html.slice(0, 2000)); } catch (e) { console.log('[interceptReplyForm] response html (full):', html); }
          const doc = new DOMParser().parseFromString(html, 'text/html');
          const response = classifySubmitResponse(doc, html);
          const successMsg = response.kind === 'success' ? response.node : null;
          const errorMsg = response.kind === 'error' ? response.node : null;
          // 打印解析到的 success / error 节点,便于调试未触发 toast 的情况
          console.log('[interceptReplyForm] parsed successMsg:', successMsg, 'errorMsg:', errorMsg);
          if (response.kind === 'fallback-error') {
            toast(response.message, 0, { type: 'error', key: 'send-error' });
            return;
          }
          if (successMsg) {
            // 与触底/手动局部刷新一致:快速即时 toast,不走默认队列
            toast(successMsg.textContent.trim() || (isReply ? '回复成功' : '发串成功'), 900, { queue: false, key: 'send-status' });
            // 服务器明确回执成功 → 本饼干进入 15s 冷却(失败不进入)
            try { startCookieCooldown(); } catch (e) {}
            const { confirmPromise, localId } = snapshotSubmittedPostHistory(fd, { isPost, isReply, form });
            // 清空输入框、图片、草稿及身份字段,并广播成功事件
            clearSuccessfulSubmitForm();
            // 重置预览框并刷新预览饼干 ID
            clearSuccessfulSubmitPreview();
            // if (isReply) {
            //   try {
            //     refreshRepliesWithSeamlessPaging(() => {
            //       try {
            //         reapplyPageEnhancements();
            //         if (window.SeamlessPaging && typeof window.SeamlessPaging.refreshAndMaybeLoadNext === 'function') {
            //           // 延迟一点再检查,避免 DOM 还没稳定
            //           setTimeout(() => {
            //             try {
            //               window.SeamlessPaging.refreshAndMaybeLoadNext();
            //             } catch (err) {
            //               console.warn('SeamlessPaging.refreshAndMaybeLoadNext 执行失败', err);
            //             }
            //           }, 200);
            //         }
            //       } catch (err) {
            //         console.warn('刷新后增强处理失败', err);
            //         // 不向上抛出,避免触发外层的 catch
            //       }
            //     });
            //   } catch (err) {
            //     console.error('refreshRepliesWithSeamlessPaging 调用失败', err);
            //     // 不向上抛出,避免触发外层的 "未知错误"
            //   }
            // } else {
            //   location.reload();
            // }
            runSuccessfulSubmitNavigation(confirmPromise);
          } else if (errorMsg) {
            const msg = errorMsg.textContent.trim() || '提交失败';
            // 服务端回执里带「冷却时间[N]s」→ 按服务器给出的剩余秒数校准显示
            // (仅在该饼干已有成功计时记录时生效,失败提交本身不创造冷却)
            try { syncCookieCooldownFromServer(msg); } catch (e) {}
            const errorKind = classifySubmitError(msg);
            const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
            // 图片格式不被服务器接受(如 WebP):转换为 PNG 后重新提交
            if (errorKind === 'image-format' && !isRetry && !prepared) {
              const fileInput = form.querySelector('input[type="file"][name="image"]');
              const file = fileInput && fileInput.files && fileInput.files[0];
              if (file) {
                const actualFormat = await detectImageFormat(file);
                console.log(`[interceptReplyForm] 格式不正确,检测到真实格式: ${actualFormat} | MIME: ${file.type} | 文件名: ${file.name}`);
                if (actualFormat === 'webp' || actualFormat === 'animated-webp' || actualFormat === 'heic') {
                  beginSubmitImageProcessing(form);
                  try {
                    const convertedFile = await runImageProcessingWithTimeout('图片格式转换', async (token) => {
                      let outFile;
                      if (actualFormat === 'heic') {
                        if (!token.cancelled) toast('服务器不接受 HEIC,正在转换为 PNG……', 3000);
                        outFile = await convertHeicToPng(file);
                        if (token.cancelled) return null;
                        console.log(`[interceptReplyForm] HEIC→PNG 转换完成: ${(file.size / 1024).toFixed(1)}KB -> ${(outFile.size / 1024).toFixed(1)}KB`);
                      } else {
                        if (!token.cancelled) toast('服务器不接受 WebP,正在将Webp转换为PNG格式……', 3000);
                        outFile = await convertWebpToAcceptedFormat(file, actualFormat === 'animated-webp');
                        if (token.cancelled) return null;
                        console.log(`[interceptReplyForm] WebP→PNG 转换完成: ${(file.size / 1024).toFixed(1)}KB -> ${(outFile.size / 1024).toFixed(1)}KB`);
                      }
                      // 转换后如果大小超限,接入压缩流程
                      if (outFile.size > SUBMIT_IMAGE_LIMIT_BYTES) {
                        if (!token.cancelled) {
                          console.log(`[interceptReplyForm] 转换后 ${(outFile.size / 1024).toFixed(1)}KB 仍超限,接入压缩流程`);
                          toast(`转换后 ${(outFile.size / 1024).toFixed(1)}KB 仍超限,正在压缩……`, 3000);
                        }
                        outFile = await compressImageForRetry(outFile, 'png', (message, duration, options) => {
                          if (!token.cancelled) toast(message, duration, options);
                        });
                        if (token.cancelled) return null;
                        console.log(`[interceptReplyForm] 压缩完成: ${(outFile.size / 1024).toFixed(1)}KB`);
                      }
                      return outFile;
                    });
                    if (!convertedFile) return;
                    resetIllegalRetryState({ clearOriginalContent: false });
                    rememberPreparedSubmitImage(file, convertedFile);
                    const newFD = cloneFormDataWithImage(fd, convertedFile);
                    toast(`格式已转换为 PNG,大小 ${(convertedFile.size / 1024).toFixed(1)}KB,正在重新提交……`, 2000);
                    await doSubmit(newFD, true);
                    return;
                  } catch (convertErr) {
                    console.error('[interceptReplyForm] 图片格式转换失败:', convertErr);
                    toast(convertErr && convertErr.message ? convertErr.message : '图片格式转换失败,请手动转换为 JPG/PNG 后再试', 0, { type: 'error' });
                    return;
                  } finally {
                    finishSubmitImageProcessing(form);
                  }
                } else {
                  // 非 WebP 格式被服务器拒绝,无法自动转换
                  toast('服务器拒绝此图片格式,请手动转换为 JPG/PNG 后重试', 4000);
                  return;
                }
              }
            }
            // 检查是否是图片大小错误
            if (errorKind === 'image-size') {
              if (!cfg.interceptReplyFormAutoCompress) {
                try {
                  toast(msg, 0, { type: 'error', key: 'send-error' });
                } catch (e) {
                  console.warn('[interceptReplyForm] toast error for image-size message:', msg, e);
                }
                return;
              }
              const fileInput = form.querySelector('input[type="file"][name="image"]');
              const file = fileInput && fileInput.files && fileInput.files[0];
              if (file && !isRetry && !prepared) {
                beginSubmitImageProcessing(form);
                try {
                  const actualFormat = await detectImageFormat(file);
                  console.log(`[interceptReplyForm] 检测到真实格式: ${actualFormat} | MIME: ${file.type} | 文件名: ${file.name}`);
                  toast(`图片大小 ${(file.size / 1024).toFixed(1)}KB(>2048KB),正在尝试自动压缩`, 3000);
                  console.log(`[interceptReplyForm] 图片大小: ${(file.size / 1024).toFixed(1)}KB,开始压缩……`);
                  const compressedFile = await runImageProcessingWithTimeout('图片压缩', (token) =>
                    compressImageForRetry(file, actualFormat, (message, duration, options) => {
                      if (!token.cancelled) toast(message, duration, options);
                    })
                  );
                  resetIllegalRetryState({ clearOriginalContent: false });
                  rememberPreparedSubmitImage(file, compressedFile);
                  const newFD = cloneFormDataWithImage(fd, compressedFile);
                  const gifCompressionDurationMs = Number(compressedFile && compressedFile.__xdexGifCompressionDurationMs);
                  const gifCompressionSeconds = Number.isFinite(gifCompressionDurationMs) && gifCompressionDurationMs >= 0
                    ? gifCompressionDurationMs / 1000
                    : null;
                  toast(`图片已压缩至 ${(compressedFile.size / 1024).toFixed(1)}KB${gifCompressionSeconds == null ? '' : `,耗时 ${gifCompressionSeconds.toFixed(2)} 秒`},正在重新提交……`, 2000);
                  await doSubmit(newFD, true);
                  return;
                } catch (compressErr) {
                  console.error('[interceptReplyForm] 图片压缩失败:', compressErr);
                  toast(compressErr && compressErr.message ? compressErr.message : '图片压缩失败,请手动压缩后再试', 0, { type: 'error' });
                  return;
                } finally {
                  finishSubmitImageProcessing(form);
                }
              } else if (isRetry || prepared) {
                toast('压缩后图片仍然超过限制,请手动压缩后再试', 3000);
                return;
              }
            }
            // 非法图像文件 + GIF:可能是无 GCT 等结构问题,尝试用 gifsicle 重新编码后重试
            if (errorKind === 'illegal-image' && !isRetry) {
              const fileInput = form.querySelector('input[type="file"][name="image"]');
              const file = fileInput && fileInput.files && fileInput.files[0];
              if (file && /\.gif$/i.test(file.name)) {
                beginSubmitImageProcessing(form);
                try {
                  console.log('[interceptReplyForm] 检测到非法GIF,尝试用gifsicle重新编码……');
                  const reencodedFile = await runImageProcessingWithTimeout(
                    'GIF 修复',
                    () => reencodeGifWithGifsicle(file),
                    '请更换图片,或手动修复/压缩后重试。'
                  );
                  rememberPreparedSubmitImage(file, reencodedFile);
                  const newFD = cloneFormDataWithImage(fd, reencodedFile);
                  toast('GIF结构已修复,正在重新提交……', 2000);
                  await doSubmit(newFD, true);
                  return;
                } catch (reencodeErr) {
                  console.error('[interceptReplyForm] GIF重编码失败:', reencodeErr);
                  toast(reencodeErr && reencodeErr.message ? reencodeErr.message : 'GIF重编码失败,请手动处理', 0, { type: 'error' });
                  return;
                } finally {
                  finishSubmitImageProcessing(form);
                }
              }
            }
            // 如果不是"含有非法词语"的特殊情况,直接提示并返回,避免被后续分支忽略
            if (errorKind !== 'illegal-word') {
              try {
                toast(msg, 0, { type: 'error', key: 'send-error' });
              } catch (e) {
                console.warn('[interceptReplyForm] toast error for message:', msg, e);
              }
              return;
            }
            if (errorKind === 'illegal-word') {
              if (cfg.interceptReplyFormUnvcode) {
                // 新增:优先判断 interceptReplyFormU200B
                if (cfg.interceptReplyFormU200B) {
                  // 新增:初始化或递增重试计数
                  form.__illegalRetryCountU200B = (form.__illegalRetryCountU200B || 0);
                  // 新增:检查是否已经重试过
                  if (form.__illegalRetryCountU200B >= 1) {
                    toast('插入零宽空格后仍提交失败,非法词语可能存在于url中,请手动处理', 0, { type: 'error', key: 'send-error' });
                    restoreOriginalSubmitContent(form);
                    form.__originalContent = null;
                    form.__illegalRetryCountU200B = 0;
                    return;
                  }
                  const currentInput = getCurrentSubmitContent(form, formData);
                  // 构造安全文本:对所有非 URL 段的中文与英文字符插入 U+200B
                  const safeText = processTextPreservingHiddenTags(currentInput, (segment) => insertZwspAfterHanAndAsciiLettersOutsideUrls(segment));
                  const newFD = createContentRetryFormData(form, safeText);
                  // 新增:递增计数
                  form.__illegalRetryCountU200B++;
                  toast('已尝试插入零宽空格模式并重试提交', 2000);
                  await doSubmit(newFD, false);
                  return;
                } else {
                  // 原有三次重试逻辑
                  const normalRetries = 2;       // 前两次正常替换
                  const fallbackRetryIndex = 2;  // 第三次(索引为2)执行保底
                  const maxRetriesAll = 3;       // 共尝试三次:0、1(正常),2(保底)
                  form.__illegalRetryCount = (form.__illegalRetryCount || 0);
                  const currentInput = getCurrentSubmitContent(form, formData);
                  if (form.__illegalRetryCount === 0) {
                    form.__originalContent = currentInput;
                  }
                  if (form.__illegalRetryCount >= maxRetriesAll) {
                    toast('unvcode替换后仍提交失败,已恢复原始文本,请手动处理', 0, { type: 'error', key: 'send-error' });
                    restoreOriginalSubmitContent(form);
                    resetCacheForFailedContent(form.__originalContent);
                    form.__originalContent = null;
                    form.__illegalRetryCount = 0;
                    return;
                  }
                  let safeText;
                  if (form.__illegalRetryCount < normalRetries) {
                    safeText = processTextPreservingHiddenTags(currentInput, (segment) => unvcodeSelective(segment));
                  } else {
                    safeText = processTextPreservingHiddenTags(currentInput, (segment) => fallbackInsertZWSP(segment));
                  }
                  const shouldRetry =
                    (form.__illegalRetryCount < normalRetries && safeText !== currentInput)
                    || (form.__illegalRetryCount === fallbackRetryIndex);
                  if (shouldRetry) {
                    toast('已尝试unvcode替换模式并重试提交', 2000);
                    const newFD = createContentRetryFormData(form, safeText);
                    form.__illegalRetryCount++;
                    await doSubmit(newFD, false);
                    return;
                  }
                  toast(msg, 0, { type: 'error', key: 'send-error' });
                }
              } else {
                toast(msg, 0, { type: 'error', key: 'send-error' });
              }
            }
          }
        })
        .catch((err) => {
          console.error('[interceptReplyForm] fetch error:', err);
          clearSubmitPreflightStatus();
          if (err && err.name === 'AbortError') {
            form.__submitUnknown = true;
            toast('提交超时:结果未知,请刷新页面确认是否已发出,避免重复提交。', 0, { type: 'error', key: 'send-error' });
            return;
          }
          if (isSubmitNetworkError(err)) {
            form.__submitUnknown = true;
            toast(SUBMIT_NETWORK_UNKNOWN_MSG, 0, { type: 'error', key: 'send-error' });
            return;
          }
          toast(formatSubmitError(err), 0, { type: 'error', key: 'send-error' });
        })
        .finally(() => {
          clearTimeout(abortTimer);
          if (form.__submitAbortTimer === abortTimer) form.__submitAbortTimer = 0;
          if (form.__submitController === controller) form.__submitController = null;
        });
      }
      // 防重复提交:__submitting 从点击发送保持到请求有确定结果(成功/失败/超时)。
      // 15s 看门狗只提示「结果未知」,不再解锁 —— 请求可能仍在飞行,解锁会让用户重复发送。
      // 真正的超时由 doSubmit 的 AbortController 负责(按上传体积放宽)。
      const SUBMIT_LOCK_TIMEOUT_MS = 30000;
      const SUBMIT_RESULT_UNKNOWN_MSG = '提交结果未知:请求超过 30 秒未返回。请刷新页面确认是否已发出,避免重复提交。';
      const SUBMIT_NETWORK_UNKNOWN_MSG = '网络异常,结果未知:请刷新页面确认是否已发出,避免重复提交。';
      const SUBMIT_IMAGE_PROCESSING_TIMEOUT_MS = 180000;
      const SUBMIT_IMAGE_PROCESSING_TIMEOUT_MSG = '发送前处理超时(180 秒),本次未发送。请手动压缩/转换图片后重试。';
      const SUBMIT_REQUEST_TIMEOUT_BASE_MS = 30000;
      const SUBMIT_REQUEST_TIMEOUT_PER_MB_MS = 15000;
      const SUBMIT_REQUEST_TIMEOUT_MAX_MS = 120000;
      function computeSubmitRequestTimeoutMs(fd) {
        let bytes = 0;
        try {
          const image = fd && typeof fd.get === 'function' ? fd.get('image') : null;
          if (image && typeof image.size === 'number' && isFinite(image.size)) bytes = image.size;
        } catch (e) { /* 取不到大小就按纯文本超时处理 */ }
        const mb = bytes > 0 ? Math.ceil(bytes / (1024 * 1024)) : 0;
        return Math.min(SUBMIT_REQUEST_TIMEOUT_MAX_MS, SUBMIT_REQUEST_TIMEOUT_BASE_MS + mb * SUBMIT_REQUEST_TIMEOUT_PER_MB_MS);
      }
      const clearSubmitLockTimer = (f) => {
        if (f && f.__submitLockTimer) {
          clearTimeout(f.__submitLockTimer);
          f.__submitLockTimer = 0;
        }
      };
      const clearSubmitAbortTimer = (f) => {
        if (f && f.__submitAbortTimer) {
          clearTimeout(f.__submitAbortTimer);
          f.__submitAbortTimer = 0;
        }
      };
      const unlockSubmit = (f) => {
        clearSubmitLockTimer(f);
        if (f) {
          clearSubmitAbortTimer(f);
          f.__submitController = null;
          f.__submitImageProcessing = false;
          f.__submitting = false;
        }
      };
      function refreshSubmitLockTimer(f) {
        if (!f || !f.__submitting || f.__submitImageProcessing) return;
        clearSubmitLockTimer(f);
        // 请求 15s 仍无结果:只标记「结果未知」并提示刷新确认,
        // 保持提交锁直到请求真正结束或被 AbortController 中止,避免重复提交
        f.__submitLockTimer = setTimeout(() => {
          f.__submitLockTimer = 0;
          if (!f.__submitting) return;
          if (f.__submitImageProcessing) return;
          f.__submitUnknown = true;
          toast(SUBMIT_RESULT_UNKNOWN_MSG, 0, { type: 'error', key: 'send-unknown' });
        }, SUBMIT_LOCK_TIMEOUT_MS);
      }
      function beginSubmitImageProcessing(f) {
        if (!f) return;
        f.__submitting = true;
        f.__submitImageProcessing = true;
        clearSubmitLockTimer(f);
      }
      function finishSubmitImageProcessing(f) {
        if (f) f.__submitImageProcessing = false;
      }
      const lockSubmit = (f) => {
        if (!f) return;
        f.__submitting = true;
        f.__submitImageProcessing = false;
        // 保留:这里不启动看门狗 —— 饼干确认弹窗的停留时间不应计入网络超时;
        // 看门狗与真实超时都由 doSubmit 在真正发请求时启动。
      };
      if (form.__submitting) {
        toast('发送中,请勿重复提交……');
        return;
      }
      if (form.__submitUnknown) {
        // 上一次请求结果未知(可能已发出):先让用户刷新确认,确认后才允许重发。
        // 未知标记保留到新请求真正开始(doSubmit)再由本次结果覆盖,
        // 避免用户确认后又在饼干弹窗点取消、旧结果仍未知却被清掉。
        const resend = window.confirm('上次提交结果未知(可能已经发出)。\n请先刷新页面确认是否已发出。\n若确定要再次发送,点「确定」。');
        if (!resend) {
          toast('已取消发送:请先刷新确认上次提交结果', 2500);
          return;
        }
      }
      lockSubmit(form);
      // ── 发送前饼干确认(含串内偏好检查) ──
      const _cookieConfirmCfg = typeof getFilterConfig === "function" ? getFilterConfig() : {};
      const _cookieConfirmEnabled = _cookieConfirmCfg.enableCookieSwitch && _cookieConfirmCfg.enableCookieConfirm;
      if (_cookieConfirmEnabled) {
        const _resto = (formData.get("resto") || "").toString().trim();
        const _tid = PageType.getThreadId(false);
        const _threadId = _resto || (_tid ? _tid.slice(0, 8) : "");
        if (_threadId && _threadId !== '20011114' && /^\d{6,8}$/.test(_threadId)) {
          const _pref = getThreadCookiePref(_threadId);
          const _doSend = () => {
            resetIllegalRetryState({ clearOriginalContent: false });
            const _ta = form.querySelector("textarea[name='content']");
            form.__originalContent = _ta ? _ta.value : (formData.get("content") || "").toString();
            toast('正在发送……', 1500, { queue: false, key: 'send-status' });
            doSubmit(formData, false).finally(() => { unlockSubmit(form); });
          };
          if (_pref) {
            const _cookieList = getCookiesList();
            if (!findCookieByHash(_cookieList, _pref.hash)) {
              // 默认饼干已失效
              toast('本串默认饼干已失效,请重新选择', 0, { type: 'error' });
              showCookieConfirmDialog(_threadId, (selectedHash) => {
                setThreadCookiePref(_threadId, selectedHash);
                if (typeof refreshCookiePrefSwitchState === 'function') refreshCookiePrefSwitchState();
                _doSend();
              }, () => { unlockSubmit(form); }, { mode: 'setDefault' });
              return;
            }
            const _curCookie = getCurrentCookie();
            const _curHash = _curCookie ? abbreviateName(_curCookie.name) : '';
            if (_curHash === _pref.hash) {
              // 匹配 → 直接发送
              _doSend();
              return;
            }
            // 不匹配 → 弹窗,预选偏好饼干
            showCookieConfirmDialog(_threadId, (selectedHash, pinnedHash) => {
              if (pinnedHash) setThreadCookiePref(_threadId, pinnedHash);
              if (typeof refreshCookiePrefSwitchState === 'function') refreshCookiePrefSwitchState();
              _doSend();
            }, () => { unlockSubmit(form); }, { preselectHash: _pref.hash });
            return;
          }
          // 无偏好 → 直接发送
          _doSend();
          return;
        }
      }
      // 每次用户触发的新提交,重置重试计数并记录当前原始内容
      resetIllegalRetryState({ clearOriginalContent: false });
      const textarea = form.querySelector('textarea[name="content"]');
      form.__originalContent = textarea
        ? textarea.value
        : (formData.get('content') || '').toString();
      toast('正在发送……', 1500, { queue: false, key: 'send-status' });
      doSubmit(formData, false).finally(() => { unlockSubmit(form); });
    }, true);
    function safeGetConfig() {
      try {
        if (typeof SettingPanel !== 'undefined' && typeof GM_getValue === 'function') {
          const defaults = SettingPanel.defaults || {};
          const saved = GM_getValue(SettingPanel.key, {}) || {};
          return Object.assign({}, defaults, saved);
          }
       } catch (e) {}
       return null;
    }
  }

  // ————— reply refresh helpers —————
    // Phase2 小步:以下两函数与闭包外共用工具同名同语义,注释掉以免遮盖外层实现
    // function getRealThreadsList(root = document) {
    //   const lists = Array.from(root.querySelectorAll('.h-threads-list'));
    //   return lists.find(el => !el.closest('.h-preview-box')) || null;
    // }
  function getCurrentPage() {
      // 对齐 originInfo / getImageViewerStartPage:?page= 优先,再 fallback path /page/N
      return PageType.getPageNum();
    }
  function getMaxPageFromPagination() {
      const paginations = Array.from(document.querySelectorAll('.uk-pagination.uk-pagination-left.h-pagination'));
      if (!paginations.length) return null;
      const last = paginations[paginations.length - 1];
      let max = 1;
      // query + path 分页链接都走 parsePaginationPageNum
      last.querySelectorAll('a[href], span[href]').forEach(a => {
        const n = parsePaginationPageNum(a.getAttribute('href') || a.href || '');
        if (n) max = Math.max(max, n);
      });
      last.querySelectorAll('li, span').forEach(el => {
        const nums = (el.textContent || '').match(/\d+/g);
        if (nums) nums.forEach(n => max = Math.max(max, parseInt(n, 10)));
      });
      return max || null;
    }
  function minimalHideEmptyTitleAndEmail(root) {
      if (!root || !root.querySelectorAll) return;
      Array.from(root.querySelectorAll('.h-threads-info-title')).forEach(el => {
        const txt = (el.textContent || '').trim();
        if (!txt || txt === '无标题' || /^无标题/i.test(txt)) {
          el.style.display = 'none';
        }
      });
      Array.from(root.querySelectorAll('.h-threads-info-email')).forEach(el => {
        const txt = (el.textContent || '').trim();
        if (!txt || txt === '无名氏' || /^无名氏/i.test(txt)) {
          el.style.display = 'none';
        }
      });
    }
  // 将拦截中间页的局部刷新保持为增量模式;wasLastPage 涨页检测仍在本包装
  function refreshRepliesWithSeamlessPaging(done, dependencies) {
      const getConfig = dependencies && dependencies.getConfig;
      const currentPage = getCurrentPage();
      const maxPage = getMaxPageFromPagination();
      const maxCloned = getMaxClonedPageInDOM();
      const resolved = resolveThreadRefreshTargetPage(maxPage, maxCloned, currentPage);
      try {
        console.log('[refreshRepliesWithSeamlessPaging] resolve', {
          href: location.href,
          currentPage,
          maxPage,
          maxCloned,
          kind: resolved && resolved.kind,
          targetPage: resolved && resolved.targetPage
        });
      } catch (e) {}
      // 旧:非末页直接 done 返回
      if (resolved.kind === 'skip') {
        try {
          console.log('[refreshRepliesWithSeamlessPaging] skip 空跑:非末页且无 cloned 目标', {
            currentPage, maxPage, maxCloned
          });
        } catch (e) {}
        if (typeof done === 'function') done();
        return;
      }
      const targetPage = resolved.targetPage;
      const list = getRealThreadsList(document);
      if (!list) {
        toast('未找到真实列表,无法刷新回复区');
        if (typeof done === 'function') done();
        return;
      }
      const pageAttr = (targetPage && targetPage > 0) ? targetPage : 0;
      const ensured = ensureThreadRepliesContainer(list, pageAttr);
      const targetReplies = ensured.targetReplies;
      if (!targetReplies) {
        toast('未找到目标回复区');
        if (typeof done === 'function') done();
        return;
      }
      if (ensured.created) {
        console.log('[refreshRepliesWithSeamlessPaging] 已创建空的回复区容器');
      }
      let fetchUrl;
      if (targetPage) {
        // cloned 目标页用 buildThreadPageUrl,避免 /t/.../page/N?page=M 混拼
        const tidMatch = PageType.getThreadId();
        const tidEl = document.querySelector('[data-threads-id]');
        const tid = (tidEl && tidEl.getAttribute('data-threads-id'))
          || tidMatch
          || null;
        if (tid && typeof buildThreadPageUrl === 'function') {
          fetchUrl = buildThreadPageUrl(tid, targetPage);
        } else {
          const url = new URL(location.href, location.origin);
          url.searchParams.set('page', String(targetPage));
          fetchUrl = url.toString();
        }
      } else {
        fetchUrl = location.href;
      }
      try { console.log('[refreshRepliesWithSeamlessPaging] fetchUrl', fetchUrl); } catch (e) {}
      // 网络层失败重试:最多 2 次退避(300ms/800ms),连续失败才走外层 catch
      const fetchTextWithRetry = (url, attempt = 0) => {
        return fetch(url, { credentials: 'include' })
          .then((res) => res.text())
          .catch((err) => {
            console.error('[refreshRepliesWithSeamlessPaging] fetch failed', {
              url,
              attempt,
              name: err && err.name,
              message: err && err.message,
              at: Date.now()
            });
            if (attempt < 2) {
              return new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 300 : 800))
                .then(() => fetchTextWithRetry(url, attempt + 1));
            }
            throw err;
          });
      };
      fetchTextWithRetry(fetchUrl)
        .then(html => {
          const doc = new DOMParser().parseFromString(html, 'text/html');
          const newList = getRealThreadsList(doc);
          if (!newList) {
            toast('未找到最新列表');
            if (typeof done === 'function') done();
            return;
          }
          const newReplies = newList.querySelector('.h-threads-item-replies');
          if (!newReplies) {
            toast('未找到最新回复区');
            if (typeof done === 'function') done();
            return;
          }
          const cfg2 = (typeof getConfig === 'function') ? getConfig() : null;
          // ★ 计时点:串内页「发送后刷新」的同步 DOM 处理(fragment 预增强 → merge → 逐节点后处理)。
          // 本路径与刷新按钮的 refreshRepliesAndCheckNext 是两条独立实现,需分别计量。
          // 网络往返已排除在计时点之外。
          const __perfToken = startupPerfDebug.begin();
          const fragment = document.createElement('div');
          fragment.innerHTML = newReplies.innerHTML;
          stripSystemTipReplies(fragment);
          try {
            if (typeof preprocessPageEnhancementsBeforeInsert === 'function') {
              preprocessPageEnhancementsBeforeInsert(fragment, cfg2);
            } else {
              if (typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail($(fragment));
              if (cfg2 && typeof applyFilters === 'function') applyFilters(cfg2, fragment);
            }
          } catch (e) {
            console.warn('预处理过滤失败', e);
          }
          const merge = appendMissingRepliesByThreadsId(targetReplies, fragment, {
            replyOnly: true,
            excludeSystemOnOld: true,
            excludeSystemOnNew: true
          });
          const appendedNodes = merge.appendedNodes;
          // ★ 借道回填(串内页):从本次 fetch 的页面提取 fid/页码,把 pending 发言历史
          // 记录的字段立即补齐(零新增请求)。fid 取表单 hidden input[name=fid]。
          try {
            const inlineFid = normalizePostHistoryFid(doc.querySelector('form input[name="fid"]') ? doc.querySelector('form input[name="fid"]').value : '');
            // 发送时所在页:用户在 /t/xxx/page/N 发送,新回复就在该页(fetch 的就是当前页)
            const inlinePage = (parseThreadHistoryUrl(location.href) || {}).page || 0;
            if (inlineFid && appendedNodes.length) {
              appendedNodes.forEach((node) => {
                const rid = String(node.getAttribute('data-threads-id') || '').trim();
                if (!rid || rid === '9999999') return;
                const store = getPostHistoryStore();
                const now = Date.now();
                Object.keys(store.items).forEach((localId) => {
                  const item = store.items[localId];
                  if (!item || item.status !== 'pending' || item.type !== 'reply') return;
                  if (String(item.id || '') !== rid) return;
                  if (now - (Number(item.submittedAt) || 0) > 180000) return;
                  const titleEl = node.querySelector('.h-threads-info-title');
                  const emailEl = node.querySelector('.h-threads-info-email');
                  // 站点渲染的「无标题/无名氏」是占位符不是真实值,不入队
                  const rawTitle = titleEl ? String(titleEl.textContent || '').trim() : '';
                  const rawEmail = emailEl ? String(emailEl.textContent || '').trim() : '';
                  postHistoryFieldPatchQueue.set(localId, {
                    fid: inlineFid,
                    forumName: getPostHistoryForumNameByFid(inlineFid) || '',
                    page: Math.max(1, Number(inlinePage) || Number(item.page) || 0),
                    title: rawTitle && rawTitle !== '无标题' ? rawTitle : '',
                    email: rawEmail && rawEmail !== '无名氏' ? rawEmail : ''
                  });
                  schedulePostHistoryFieldPatchFlush();
                });
              });
            }
          } catch (e) {}
          try {
            if (typeof hideEmptyTitleAndEmail === 'function') {
              appendedNodes.forEach(n => { try { hideEmptyTitleAndEmail($(n)); } catch (_) {} });
            }
            if (cfg2 && typeof applyFilters === 'function') {
              appendedNodes.forEach(n => { try { applyFilters(cfg2, n); } catch (_) {} });
            }
            if (typeof enablePostExpand === 'function') {
              appendedNodes.forEach(n => { try { enablePostExpand(n); } catch (_) {} });
            }
          } catch (e) {}
          // ★ 计时收尾:只覆盖响应到达后的同步 DOM 工作,不含下面的 50ms 异步增强段
          // (异步段已由 applyPageEnhancements.total 单独记录)
          startupPerfDebug.finish('refresh.postReply.syncDom', __perfToken, () => ({
            replies: targetReplies.querySelectorAll ? targetReplies.querySelectorAll('.h-threads-item-reply').length : -1,
            appended: (appendedNodes && appendedNodes.length) || 0,
            domPages: document.querySelectorAll('.h-threads-item-replies[data-cloned-page]').length
          }));
          setTimeout(() => {
            try {
              if (typeof applyPageEnhancements === 'function') {
                applyPageEnhancements(targetReplies, cfg2 || (typeof getConfig === 'function' ? getConfig() : null), appendedNodes);
              } else {
                try { if (typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail($(targetReplies)); } catch (e) {}
                try { if (typeof highlightPO === 'function') highlightPO(targetReplies); } catch (e) {}
                try { if (typeof enableHDImageAndLayoutFix === 'function') enableHDImageAndLayoutFix(document); } catch (e) {}
                try { if (typeof enableHDImage === 'function') enableHDImage(document); } catch (e) {}
                try { if (typeof initContent === 'function') initContent(targetReplies); } catch (e) {}
              }
            } catch (e) {}
          }, 50);
          // wasLastPage 需在写回前读取 oldPag
          const oldPagsBefore = document.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
          const oldPagBefore = oldPagsBefore.length ? oldPagsBefore[oldPagsBefore.length - 1] : null;
          let wasLastPage = false;
          if (oldPagBefore) {
            const oldNextLi = Array.from(oldPagBefore.querySelectorAll('li')).find(li =>
              /下一页|下页|Next|›|»|→/i.test(li.textContent.trim())
            );
            wasLastPage = !!(oldNextLi &&
              oldNextLi.classList.contains('uk-disabled') &&
              !oldNextLi.querySelector('a'));
          }
          const newPags = doc.querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination');
          const newPag = newPags.length ? newPags[newPags.length - 1] : null;
          prepareAndSyncBottomPagination(doc);
          if (wasLastPage && newPag) {
            const newNextLi = Array.from(newPag.querySelectorAll('li')).find(li =>
              /下一页|下页|Next|›|»|→/i.test(li.textContent.trim())
            );
            const hasNewPage = !!(newNextLi &&
              !newNextLi.classList.contains('uk-disabled') &&
              !!newNextLi.querySelector('a'));
            if (hasNewPage) {
              const nextLink = newNextLi.querySelector('a');
              const nextPageNum = nextLink
                ? parsePaginationPageNum(nextLink.getAttribute('href') || nextLink.href || '')
                : null;
              if (nextPageNum) {
                // 发送成功后的单次刷新无代际并发,不引用其他函数的 activeGeneration(跨作用域 ReferenceError)
                showRefreshStatus('发现' + nextPageNum + '页,正在加载……', 700);
                if (window.SeamlessPaging && typeof window.SeamlessPaging.loadNext === 'function') {
                  setTimeout(() => {
                    window.SeamlessPaging.loadNext();
                  }, 100);
                }
              }
            }
          }
          // 过滤兜底改为 scoped:只重算本次新增的回复区。全文档调用会对整页所有回复
          // 重跑过滤/PO 注释列重算(串越长越慢),且 50ms 后 applyPageEnhancements(targetReplies)
          // 内部还会 scoped 调一次,全文档版是冗余的(发送后既有回复的过滤状态不变,无需重算)。
          try {
            if (cfg2 && typeof applyFilters === 'function') {
              try { refreshFilterDisplay(cfg2, targetReplies); } catch (e) {}
            }
          } catch (e) {}
          if (typeof done === 'function') done();
        })
        .catch((err) => {
          console.error('[refreshRepliesWithSeamlessPaging] refresh failed', {
            url: fetchUrl,
            name: err && err.name,
            message: err && err.message,
            at: Date.now()
          });
          toast('刷新回复区失败,可能已有新回复,请手动刷新');
          if (typeof done === 'function') done({ ok: false, reason: 'fetch-failed' });
        });
    }
  function isCookieDropdownShortcut(e) {
    return !!(e && e.ctrlKey && (e.code === 'Backslash' || e.code === 'IntlBackslash' || e.key === '\\'));
  }
  function logCookieDropdownShortcutStage(stage, detail) {
    try {
      console.log('[XDEX cookie shortcut]', stage, Object.assign({ runtime: XDEX_RUNTIME && XDEX_RUNTIME.kind }, detail || {}));
    } catch (e) {}
  }
  function openCookieDropdownFromShortcut(focusBackTarget) {
    const dropdown = document.getElementById('cookie-dropdown');
    if (!dropdown) {
      logCookieDropdownShortcutStage('dropdown-missing', {
        activeTag: document.activeElement && document.activeElement.tagName,
        textareas: document.querySelectorAll('form textarea[name="content"]').length,
        switcher: !!document.getElementById('cookie-switcher-ui')
      });
      return false;
    }
    dropdown.__focusBackTarget = focusBackTarget || document.activeElement || null;
    dropdown.__openedValue = dropdown.value;
    dropdown.focus();
    logCookieDropdownShortcutStage('dropdown-focus', {
      options: dropdown.options ? dropdown.options.length : 0,
      value: dropdown.value,
      hasShowPicker: typeof dropdown.showPicker === 'function'
    });
    if (XDEX_RUNTIME && XDEX_RUNTIME.kind === 'extension') {
      return openCookieShortcutMenu(dropdown, focusBackTarget);
    }
    if (typeof dropdown.showPicker === 'function') {
      try {
        dropdown.showPicker();
        logCookieDropdownShortcutStage('showPicker-called');
        return true;
      } catch (e) {
        logCookieDropdownShortcutStage('showPicker-failed', { message: e && e.message ? e.message : String(e) });
      }
    }
    const mouseDownEvent = new MouseEvent('mousedown', {
      bubbles: true,
      cancelable: true,
      view: window
    });
    dropdown.dispatchEvent(mouseDownEvent);
    logCookieDropdownShortcutStage('fallback-mousedown-dispatched');
    return true;
  }
  function installCookieDropdownShortcutHandler() {
    if (installCookieDropdownShortcutHandler.__installed) return;
    installCookieDropdownShortcutHandler.__installed = true;
    logCookieDropdownShortcutStage('handler-installed');
    function onCookieDropdownShortcutKeydown(e) {
      if (e && e.ctrlKey) {
        logCookieDropdownShortcutStage('ctrl-keydown', { key: e.key, code: e.code, targetTag: e.target && e.target.tagName });
      }
      if (!isCookieDropdownShortcut(e)) return;
      logCookieDropdownShortcutStage('shortcut-matched', { key: e.key, code: e.code });
      if (!openCookieDropdownFromShortcut(document.activeElement)) return;
      e.preventDefault();
      e.stopPropagation();
      if (typeof e.stopImmediatePropagation === 'function') {
        e.stopImmediatePropagation();
      }
    }
    document.addEventListener('keydown', onCookieDropdownShortcutKeydown, true);
  }
  // 绑定 Ctrl+Enter 快捷提交表单 + 全局打开浮窗
  function bindCtrlEnter(ta) {
    if (!ta || ta.__ctrlEnterBound) return;
    const form = ta.closest('form');
    if (!form) return;
    ta.__ctrlEnterBound = true;
    // 焦点在 textarea 内时:Ctrl+Enter 提交,Ctrl+\ 打开 cookie 下拉框
    ta.addEventListener('keydown', function (e) {
      // Ctrl+Enter 提交表单
      if (e.ctrlKey && e.key === 'Enter') {
        e.preventDefault();
        e.stopPropagation();
        if (typeof e.stopImmediatePropagation === 'function') {
          e.stopImmediatePropagation();
        }
        try { form.requestSubmit(); } catch (_) { form.submit(); }
      }
      // Ctrl+\ 打开 cookie 下拉框
      if (isCookieDropdownShortcut(e)) {
        if (openCookieDropdownFromShortcut(ta)) {
          e.preventDefault();
          e.stopPropagation();
          if (typeof e.stopImmediatePropagation === 'function') {
            e.stopImmediatePropagation();
          }
        }
      }
      // Ctrl+/ 打开颜文字选择框
      if (e.ctrlKey && e.key === '/') {
        e.preventDefault();
        e.stopPropagation();
        const kaomojiTrigger = document.querySelector('.kaomoji-trigger');
        if (kaomojiTrigger) {
          kaomojiTrigger.click();
        }
      }
    });
    // 全局监听:Ctrl+Enter 打开浮窗
    document.addEventListener('keydown', function (e) {
      // Ctrl + Enter 打开浮窗(已有逻辑)
      if (e.ctrlKey && e.key === 'Enter') {
        const active = document.activeElement;
        const isTextarea = active && active.tagName === 'TEXTAREA';
        if (!isTextarea) {
          e.preventDefault();
          e.stopPropagation();
          const replyBtn = document.querySelector('.hld__docker [data-type="REPLY"]');
          if (replyBtn) replyBtn.click();
        }
      }
    }, false);
  }

  /* --------------------------------------------------
   * tag 12. 高亮Po主+回复表单UI调整
   * -------------------------------------------------- */
  // 高亮 Po 主(内置并先执行楼层编号)
  function highlightPO(root) {
    const scope = root || document;
    return startupPerfDebug.measure('highlightPO', () => {
    const poTextColor  = '#00FFCC'; // Po 本体颜色
    const iconWidthEm  = 3.0;       // 所有图标统一宽度
    // 子函数:先为当前页面所有回复区编号(原 updateReplyNumbers 逻辑)
    function updateReplyNumbersLocal() {
      const repliesList = scope.querySelectorAll
        ? Array.from(scope.querySelectorAll('.h-threads-item-replies')).concat(scope.matches && scope.matches('.h-threads-item-replies') ? [scope] : [])
        : Array.from(document.querySelectorAll('.h-threads-item-replies'));
      repliesList.forEach(replies => {
        let effectiveCount = 0;
        replies.querySelectorAll('.h-threads-item-reply-icon').forEach(icon => {
          const reply = icon.closest('[data-threads-id]');
          if (!reply) return;
          if (reply.getAttribute('data-threads-id') === '9999999') {
            // 特殊:小提示串号 -> 编号 0
            icon.textContent = circledNumber(0);
          } else {
            // 普通回复 -> 依次递增
            effectiveCount++;
            icon.textContent = `『${effectiveCount}』`;
          }
        });
      });
    }
    // 统一设置所有回复图标的宽度与基础样式
    const icons = scope.querySelectorAll ? scope.querySelectorAll('.h-threads-item-reply-icon') : document.querySelectorAll('.h-threads-item-reply-icon');
    icons.forEach(icon => {
      icon.style.display = 'inline-block';
      icon.style.width = iconWidthEm + 'em';
      icon.style.textAlign = 'center';
      icon.style.position = 'relative';
      icon.style.fontWeight = 'normal';
    });
    // 先编号,再做 Po 标替换
    updateReplyNumbersLocal();
    // 替换 PO 回复的数字为 Po,并加角标
    const replies = scope.querySelectorAll ? scope.querySelectorAll('.h-threads-item-reply') : document.querySelectorAll('.h-threads-item-reply');
    replies.forEach(reply => {
      const main = reply.querySelector('.h-threads-item-reply-main');
      const icon = reply.querySelector('.h-threads-item-reply-icon');
      if (!main || !icon) return;
      const isPO = !!main.querySelector('span.uk-text-primary.uk-text-small');
      if (!isPO) return;
      let html = icon.innerHTML;
      const m = html.match(/『(\d+)』/);
      if (!m) return;
      const originalNumber = m[1]; // 原数字
      const poHTML = `『<span style="color:${poTextColor}; font-weight:bold">Po</span>』`;
      // 替换数字为 Po
      html = html.replace(m[0], poHTML);
      icon.innerHTML = html;
      // 角标(避免重复添加)
      if (!icon.querySelector('.po-n-badge')) {
        const badge = document.createElement('span');
        badge.className = 'po-n-badge';
        badge.textContent = originalNumber;
        Object.assign(badge.style, {
          position: 'absolute',
          top: '-0.55em',
          right: '-0.6em',
          fontSize: '10px',
          lineHeight: '1',
          fontWeight: '600',
          color: 'initial',
          background: 'transparent',
          pointerEvents: 'none',
        });
        icon.appendChild(badge);
      }
    });
    }, () => startupPerfDebug.summarizeRoot(document));
  }
  // 初次执行
  highlightPO();
  // 正文服务端等效字数:首尾 U+0020/换行不计;内部换行按 8;其余字符按 1
  const POST_FORM_CONTENT_SERVER_MAX_LENGTH = 5000;
  function countPostContentServerChars(raw) {
    let s = String(raw || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
    s = s.replace(/^[ \n]+/, '').replace(/[ \n]+$/, '');
    if (!s) return 0;
    let n = 0;
    for (let i = 0; i < s.length; i++) {
      n += s.charCodeAt(i) === 10 ? 8 : 1;
    }
    return n;
  }
  // IME(拼音/日文等)组字期间不计算字数:
  //   组字过程中浏览器会带着「拼音串」连续触发 input(如输入 nihao 时值为 "nihao"),
  //   此时统计会出现数字乱跳、并可能误判超限标红;等 compositionend(候选词上屏)后再算一次。
  //   兼容两种事件顺序(compositionend→input 或 input→compositionend):组字中一律跳过,
  //   上屏时统一补算,两种顺序下结果都正确。
  function bindImeAwareCharCounter(el, update) {
    if (!el || typeof update !== 'function') return;
    if (el.__xdexImeCounterGuarded) el.removeEventListener('input', el.__xdexImeCounterGuarded);
    if (el.__xdexImeCounterStart) el.removeEventListener('compositionstart', el.__xdexImeCounterStart);
    if (el.__xdexImeCounterEnd) el.removeEventListener('compositionend', el.__xdexImeCounterEnd);
    el.__xdexImeCounterStart = () => { el.__xdexImeComposing = true; };
    el.__xdexImeCounterEnd = () => { el.__xdexImeComposing = false; update(); };
    el.__xdexImeCounterGuarded = () => { if (el.__xdexImeComposing) return; update(); };
    el.addEventListener('compositionstart', el.__xdexImeCounterStart);
    el.addEventListener('compositionend', el.__xdexImeCounterEnd);
    el.addEventListener('input', el.__xdexImeCounterGuarded);
    el.__xdexImeComposing = false;
    update();
  }
  //回复表单UI调整
  function enhancePostFormLayout(formOpt) {
     // 浮窗内表单优先;允许 open() 传入当前 form,避免 early 时扫到错误节点
     const form = formOpt
      || document.querySelector('.qp-body form[action*="doReplyThread"], .qp-body form[action*="doPostThread"]')
      || document.querySelector('form[action*="doReplyThread"], form[action*="doPostThread"]');
      if (!form) return;
      // 定位“标题”行与“颜文字”行
      const allRows = Array.from(form.querySelectorAll('.h-post-form-grid'));
      let titleRow = null, emoticonRow = null;
      for (const row of allRows) {
        const titleText = row.querySelector('.h-post-form-title')?.textContent?.trim() || '';
        if (titleText === '标题') titleRow = row;
        if (row.querySelector('.kaomoji-trigger') || row.querySelector('#h-emot-select')) {
          emoticonRow = row;
        }
      }
      // 1) 先把送出按钮移到“颜文字”行,并让整行用 flex 不换行,按钮推到行最右
      // 已搬过则跳过,避免 early open / ready 重复搬
      if (titleRow && emoticonRow && !emoticonRow.querySelector('input[type="submit"]')) {

        const sendBtnCell = titleRow.querySelector('.h-post-form-option');
        const sendBtn = sendBtnCell?.querySelector('input[type="submit"]');
        if (sendBtn) {
          // 让“颜文字”整行用 flex 布局,禁止换行,垂直居中
          Object.assign(emoticonRow.style, {
            display: 'flex',
            flexWrap: 'nowrap',
            alignItems: 'center',
            width: '100%'
          });
          // 创建一个右侧容器,使用 margin-left:auto 将其推到最右
          const btnWrapper = document.createElement('div');
          Object.assign(btnWrapper.style, {
            marginLeft: 'auto',
            display: 'flex',
            alignItems: 'center',
            gap: '6px'
          });
          const contentTextarea = form.querySelector('textarea[name="content"].h-post-form-textarea');
          if (contentTextarea) {
            let contentCounter = btnWrapper.querySelector('.xdex-post-content-char-count');
            if (!contentCounter) {
              contentCounter = document.createElement('span');
              contentCounter.className = 'xdex-post-content-char-count';
              // 「字数:」前缀固定不动,数字放进定宽盒子(4 位,对应上限 5000)左对齐:
              //   · 字数 0 → 42 → 128 → 5000 时,前缀与左侧元素都不移动
              //   · 若整块右对齐,位数增加时前缀会向左推,出现左右抖动
              contentCounter.style.cssText = 'font-size:12px;color:#888;white-space:nowrap;flex:0 0 auto;'
                + 'display:inline-flex;align-items:baseline;';
              contentCounter.appendChild(document.createTextNode('字数:'));
              const numSpan = document.createElement('span');
              numSpan.className = 'xdex-post-content-char-count-num';
              // 定宽 4ch(对应上限 5000)+ 内部「右对齐」:
              //   · 数字贴住右侧(靠右贴边),前缀「字数:」与右侧「送出」都不移动
              //   · 盒子宽度固定 → 0 → 42 → 5000 时布局完全静止
              numSpan.style.cssText = 'display:inline-block;min-width:4ch;text-align:right;'
                + 'font-variant-numeric:tabular-nums;font-feature-settings:\'tnum\' 1;';
              contentCounter.appendChild(numSpan);
              btnWrapper.appendChild(contentCounter);
            }
            const updateContentCounter = () => {
              const len = countPostContentServerChars(contentTextarea.value);
              const numSpan = contentCounter.querySelector('.xdex-post-content-char-count-num');
              // 只更新数字盒(前缀「字数:」不动,宽度由定宽盒锁定)
              if (numSpan) numSpan.textContent = String(len);
              else contentCounter.textContent = '字数:' + len;
              contentCounter.style.color = len > POST_FORM_CONTENT_SERVER_MAX_LENGTH ? '#e53935' : '#888';
            };
            // 拼音组字期间不计数(见 bindImeAwareCharCounter)
            bindImeAwareCharCounter(contentTextarea, updateContentCounter);
          }
          btnWrapper.appendChild(sendBtn);
          // 将按钮容器添加到“颜文字”行
          emoticonRow.appendChild(btnWrapper);
          // 表单重建后冷却提示需重新挂到「字数」左侧(切饼干/刷新页面后仍显示剩余时间)
          try { renderCookieCooldown(); } catch (e) {}
        }
      }
      // 2) 折叠「标题 / 名称(含管理员) / E-mail」为一个折叠面板,并固定顺序
      // 重新抓一次,避免移动节点导致 NodeList 顺序问题
      const freshRows = Array.from(form.querySelectorAll('.h-post-form-grid'));
      const norm = (s) => String(s || '').replace(/[\s-]+/g, '').toLowerCase();
      const rowMap = new Map();
      for (const row of freshRows) {
        const label = row.querySelector('.h-post-form-title')?.textContent || '';
        const k = norm(label);
        if (k === '标题') rowMap.set('title', row);
        else if (k === '名称') rowMap.set('name', row); // 原文“名 称”会被规整成“名称”
        else if (k === 'email') rowMap.set('email', row); // 原文 “E-mail”
      }
      const rowsToCollapse = ['title', 'name', 'email']
        .map(k => rowMap.get(k))
        .filter(Boolean);
      // 关闭浏览器推荐填充(title/name/email)
      form.setAttribute('autocomplete', 'off');
      form.setAttribute('data-form-type', 'other');
      const noPersistInputs = rowsToCollapse.flatMap(row =>
        Array.from(row.querySelectorAll('input[type="text"], textarea'))
      );
      const POST_FORM_OPTIONAL_MAX_LENGTH = 15;
      const POST_FORM_EMAIL_MAX_LENGTH = 100;
      noPersistInputs.forEach(input => {
        input.setAttribute('autocomplete', 'off');
        input.setAttribute('autocapitalize', 'off');
        input.setAttribute('autocorrect', 'off');
        input.setAttribute('spellcheck', 'false');
        input.setAttribute('aria-autocomplete', 'none');
        input.setAttribute('data-lpignore', 'true');
        input.setAttribute('data-form-type', 'other');
        // 标题/名称:15 字,超限红字提醒;E-mail:100 字,maxlength 拦住后不再标红
        const isEmailInput = (input.getAttribute('name') || '').toLowerCase() === 'email';
        const maxLen = isEmailInput ? POST_FORM_EMAIL_MAX_LENGTH : POST_FORM_OPTIONAL_MAX_LENGTH;
        if (isEmailInput) {
          input.setAttribute('maxlength', String(POST_FORM_EMAIL_MAX_LENGTH));
        } else {
          // 标题/名称保持可输入并超限提示,不强制 maxlength 卡住
          input.removeAttribute('maxlength');
        }
        const inputCell = input.closest('.h-post-form-input');
        if (inputCell) {
          inputCell.style.position = 'relative';
          inputCell.style.display = 'flex';
          inputCell.style.alignItems = 'center';
          inputCell.style.minWidth = '0';
          input.style.flex = '1 1 auto';
          input.style.minWidth = '0';
          // 右侧留空给绝对定位的计数器,避免输入文字被遮挡
          input.style.paddingRight = '64px';
        }
        let counter = input.nextElementSibling && input.nextElementSibling.classList?.contains('xdex-post-form-char-count')
          ? input.nextElementSibling
          : null;
        if (!counter) {
          counter = document.createElement('span');
          counter.className = 'xdex-post-form-char-count';
          counter.style.cssText = 'font-size:12px;color:#888;white-space:nowrap;position:absolute;right:6px;top:50%;transform:translateY(-50%);pointer-events:none;';
          input.insertAdjacentElement('afterend', counter);
        }
        const updateCounter = () => {
          const len = String(input.value || '').length;
          counter.textContent = len + '/' + maxLen;
          // E-mail 到 100 已被 maxlength 拦住,保持灰色;标题/名称超 15 才标红
          counter.style.color = (!isEmailInput && len > maxLen) ? '#e53935' : '#888';
        };
        // 拼音组字期间不计数(见 bindImeAwareCharCounter)
        bindImeAwareCharCounter(input, updateCounter);
      });
      // 已折叠过就不要再包一层(early open 与 ready 都会跑)
      // 若 toggle 丢失或 Utils.collapse 曾被 .qp-body 守卫挡掉,则补折叠;并清理重复按钮
      if (rowsToCollapse.length) {
        let wrapper = form.querySelector('.collapse-wrapper');
        if (!wrapper) {
          wrapper = document.createElement('div');
          wrapper.className = 'collapse-wrapper';
          wrapper.style.width = '100%';
          rowsToCollapse[0].before(wrapper);
          rowsToCollapse.forEach(r => wrapper.appendChild(r));
        }
        // 注意:Utils.collapse 展开后文案会变成「点击折叠」,不再含「可选项」
        // 若仍只按 /可选项/ 识别,会误判 toggle 丢失 → removeData 后再次 collapse → 多出一个按钮
        const isOptionalToggle = (el) => {
          if (!el || !el.classList) return false;
          if (!el.classList.contains('xdex-placeholder') || !el.classList.contains('xdex-generic-toggle')) return false;
          if (el.dataset && el.dataset.xdexOptionalToggle === '1') return true;
          const t = el.textContent || '';
          if (/可选项/.test(t)) return true;
          // 展开态:文案只剩「点击折叠/点击展开」,靠紧邻 collapse-wrapper 判定
          const next = el.nextElementSibling;
          if (next && next.classList && next.classList.contains('collapse-wrapper')
              && /点击折叠|点击展开/.test(t)) return true;
          return false;
        };
        // 合法位置:wrapper 前(Utils.collapse 默认)
        // 兼容旧逻辑误挪到 form 前(wrap 内)的情况,只保留一个
        const nearby = [];
        if (isOptionalToggle(wrapper.previousElementSibling)) nearby.push(wrapper.previousElementSibling);
        if (form.previousElementSibling && form.previousElementSibling !== wrapper.previousElementSibling
            && isOptionalToggle(form.previousElementSibling)) {
          nearby.push(form.previousElementSibling);
        }
        // 浮窗 wrap 内再扫一遍,防止残留重复
        const host = form.closest('.qp-content-wrap') || form.parentElement;
        if (host && host.querySelectorAll) {
          host.querySelectorAll('.xdex-placeholder.xdex-generic-toggle').forEach((el) => {
            if (isOptionalToggle(el) && !nearby.includes(el)) nearby.push(el);
          });
        }
        let keepToggle = nearby.find((el) => el.nextElementSibling === wrapper) || nearby[0] || null;
        nearby.forEach((el) => {
          if (el !== keepToggle) el.remove();
        });
        // 若误挪到 form 外,搬回 wrapper 前,保证位置正确且 hasToggle 判定稳定
        if (keepToggle && keepToggle.nextElementSibling !== wrapper) {
          wrapper.before(keepToggle);
        }
        if (typeof Utils !== 'undefined' && typeof Utils.collapse === 'function' && typeof $ === 'function') {
          const $wrapper = $(wrapper);
          if (keepToggle) {
            keepToggle.dataset.xdexOptionalToggle = '1';
            keepToggle.style.display = '';
            // 已有 toggle 时:只同步标记,不要在用户展开后把内容又藏回去
            // Utils.collapse 的 data 在展开后仍会保留;真正展开/收起交给原 click 处理
            if (!$wrapper.data('xdex-collapsed') && !$wrapper.is(':visible')) {
              $wrapper.hide().data('xdex-collapsed', true);
              wrapper.classList.add('xdex-generic-collapsed');
            } else if ($wrapper.data('xdex-collapsed') || $wrapper.is(':hidden')) {
              wrapper.classList.add('xdex-generic-collapsed');
            }
          } else {
            // 真的没有 toggle 才补;若 data 还在但按钮丢了,先清再 fold,避免永久卡死
            if ($wrapper.data('xdex-collapsed')) {
              $wrapper.removeData('xdex-collapsed');
              wrapper.classList.remove('xdex-generic-collapsed');
              $wrapper.show();
            }
            const $ph = Utils.collapse($wrapper, '可选项');
            const phEl = ($ph && $ph[0]) || wrapper.previousElementSibling;
            if (phEl && phEl.classList && phEl.classList.contains('xdex-generic-toggle')) {
              phEl.dataset.xdexOptionalToggle = '1';
            }
          }
        }
      }
      form.dataset.xdexPostFormLayoutEnhanced = '1';
    }

  /* --------------------------------------------------
   * tag 13. 颜文字增强-光标处插入/选择框优化/额外颜文字拓展
   * -------------------------------------------------- */
  function kaomojiEnhancer() {
      const SELECTOR = '#h-emot-select';
      let enhanceScheduled = false;
      function runKaomojiEnhancePass() {
        initInsertAtCaret();      // 功能 1:颜文字插入光标处
        extendKaomojiSet();       // 功能 3:颜文字样式拓展,先补稳定 key
        optimizeSelectorStyle();  // 功能 2:选择框样式优化,基于稳定 key 渲染排序
      }
      function scheduleKaomojiEnhancePass() {
        if (enhanceScheduled) return;
        enhanceScheduled = true;
        setTimeout(() => {
          enhanceScheduled = false;
          runKaomojiEnhancePass();
        }, 0);
      }
      function observeKaomojiSelects() {
        if (kaomojiEnhancer.__selectObserverInstalled) return;
        const observeRoot = document.body || document.documentElement;
        if (!observeRoot) return;
        kaomojiEnhancer.__selectObserverInstalled = true;
        const observer = new MutationObserver(mutations => {
          for (const mutation of mutations) {
            for (const node of mutation.addedNodes || []) {
              if (!node || node.nodeType !== 1) continue;
              if ((node.matches && node.matches(SELECTOR)) || (node.querySelector && node.querySelector(SELECTOR))) {
                scheduleKaomojiEnhancePass();
                return;
              }
            }
          }
        });
        observer.observe(observeRoot, { childList: true, subtree: true });
      }
      // 初始化所有功能
      runKaomojiEnhancePass();
      observeKaomojiSelects();
      /**
       * 功能 1:选择颜文字后插入到光标位置
       */
      // BUG:win7与部分win10环境下颜文字插入两次。
      function initInsertAtCaret() {
        const SELECTOR = '#h-emot-select';
        const TA_SELECTOR = 'textarea.h-post-form-textarea[name="content"]';
        document.querySelectorAll(SELECTOR).forEach(select => {
            if (select.dataset.kaoBound === '1') return;
            select.dataset.kaoBound = '1';
            const form = select.closest('form');
            const textarea = form ? form.querySelector(TA_SELECTOR) : null;
            if (!textarea) return;
            let lastStart = 0;
            let lastEnd = 0;
            // 记录光标位置
            const remember = () => {
                lastStart = textarea.selectionStart ?? lastStart;
                lastEnd = textarea.selectionEnd ?? lastEnd;
            };
            ['keyup', 'mouseup', 'select', 'input', 'focus', 'blur'].forEach(ev =>
                textarea.addEventListener(ev, remember, true)
            );
            // 只在 select 上监听 focus 相关事件来记录位置
            ['focus', 'mousedown'].forEach(ev =>
                select.addEventListener(ev, remember, true)
            );
            // 防抖保护
            let isInserting = false;
            // 用 change 事件来触发插入
            select.addEventListener('change', e => {
              e.stopImmediatePropagation();
              e.preventDefault();
              e.stopPropagation();
              if (isInserting) return;
              const val = select.value;
              if (!val) return;
              // ★ 在插入前强制记忆一次光标位置
              remember();
              isInserting = true;
              insertAtCaret(textarea, val, lastStart, lastEnd);
              setTimeout(() => {
                select.value = '';
                isInserting = false;
              }, 50);
              textarea.focus();
            }, true);
            // 移除 change 事件的监听,避免重复触发
            select.addEventListener('change', function(e) {
                e.stopImmediatePropagation();
                e.preventDefault();
                e.stopPropagation();
            }, true);
            // 移除原生 jQuery change handler,防止双重插入
            try {
                const jqEvents = $._data(select, 'events');
                if (jqEvents && jqEvents.change) {
                    delete jqEvents.change;
                }
            } catch (e) {}
            function insertAtCaret(textarea, text, selStart, selEnd) {
                // 记录插入前的滚动位置
                const prevScrollTop = textarea.scrollTop;
                // 确定插入位置
                let start = Number.isInteger(selStart) ? selStart : textarea.selectionStart;
                let end   = Number.isInteger(selEnd)   ? selEnd   : textarea.selectionEnd;
                if (!Number.isInteger(start) || !Number.isInteger(end)) {
                    start = end = textarea.value.length;
                }
                // 优先用 execCommand 插入(保留浏览器撤销历史,支持 Ctrl+Z)
                textarea.focus();
                textarea.setSelectionRange(start, end);
                const success = document.execCommand('insertText', false, text);
                if (!success) {
                    // 回退:execCommand 不可用时走原逻辑
                    const before = textarea.value.slice(0, start);
                    const after  = textarea.value.slice(end);
                    textarea.value = before + text + after;
                    const newPos = start + text.length;
                    textarea.setSelectionRange(newPos, newPos);
                    textarea.dispatchEvent(new Event('input', { bubbles: true }));
                }
                // 恢复滚动条位置
                textarea.scrollTop = prevScrollTop;
                // 更新记忆位置(execCommand 后光标自动在插入文本末尾)
                lastStart = lastEnd = textarea.selectionStart;
            }
        });
    }
      /**
       * 功能 2:颜文字选择框样式优化
       */
      function optimizeSelectorStyle() {
        const GAP = 4;               // 单元格间距(缩小)
        const CHAR_W = 14;           // 每个字宽度(px)
        const CHAR_H = 16;           // 每个字高度(px)
        const PAD = 6;               // 浮窗内边距(缩小)
        const ITEM_W = CHAR_W * 6 + 6; // 大约半个长颜文字宽度
        const ITEM_H = CHAR_H * 2 + 4; // 不超过两行字高
        const KAO_STATS_KEY = 'kaomojiUsageStats';
        let kaomojiStatsReadable = true;
        function getSortMode() {
          try {
            const stateMode = SettingPanel && SettingPanel.state && SettingPanel.state.kaomojiSort;
            if (stateMode === 'default' || stateMode === 'recent' || stateMode === 'freq') return stateMode;
          } catch (e) {}
          try {
            const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
            const mode = cfg.kaomojiSort || 'default';
            return (mode === 'default' || mode === 'recent' || mode === 'freq') ? mode : 'default';
          } catch (e) {
            return 'default';
          }
        }
        function loadKaomojiStats() {
          try {
            const v = GM_getValue(KAO_STATS_KEY, {});
            kaomojiStatsReadable = !!(v && typeof v === 'object');
            return kaomojiStatsReadable ? v : {};
          } catch (e) {
            kaomojiStatsReadable = false;
            return {};
          }
        }
        function saveKaomojiStats(stats) {
          if (!kaomojiStatsReadable) return;
          try { GM_setValue(KAO_STATS_KEY, stats || {}); } catch (e) {}
        }
        function normalizeKaomojiValue(v) {
          return String(v == null ? '' : v).replace(/\r\n/g, '\n');
        }
        function kaomojiKeyFromOption(opt) {
          const stableKey = (opt?.dataset?.kaoKey || '').trim();
          if (stableKey) return `sk:${stableKey}`;
          const text = (opt?.textContent || '').trim();
          const value = normalizeKaomojiValue(opt?.value);
          // 用“显示文本 + 实际值”作为唯一键,避免脚本扩展颜文字(尤其富文本)统计丢失或冲突
          return `k:${text}\nv:${value}`;
        }
        // 同一颜文字可能同时存在多种历史 key:
        // - sk:extra:xxx / sk:rich:xxx(扩展注入后)
        // - k:显示文本\nv:实际值(注入前/原生 option)
        // - 仅显示文本(更旧版本)
        // 排序/记录时必须合并,否则“最近”会看起来乱跳
        function listKaomojiStatAliases(opt) {
          const label = (opt?.textContent || '').trim();
          const value = normalizeKaomojiValue(opt?.value);
          const aliases = [];
          const push = (k) => {
            if (!k || aliases.includes(k)) return;
            aliases.push(k);
          };
          push(kaomojiKeyFromOption(opt));
          if (label) {
            push(label); // legacy
            push(`k:${label}\nv:${value}`);
            // 富文本插入时 value 可能带首尾换行,兼容去换行后的 key
            const bareVal = value.replace(/^\n+/, '').replace(/\n+$/, '');
            if (bareVal !== value) push(`k:${label}\nv:${bareVal}`);
            // 扩展项稳定 key 的两种常见形态
            push(`sk:extra:${label}`);
            push(`sk:rich:${label}`);
          }
          const stableKey = (opt?.dataset?.kaoKey || '').trim();
          if (stableKey) push(`sk:${stableKey}`);
          return aliases;
        }
        function readKaomojiStatEntry(stats, key) {
          const st = stats && key ? stats[key] : null;
          if (!st || typeof st !== 'object') return { count: 0, lastUsed: 0 };
          return {
            count: Number.isFinite(Number(st.count)) ? Number(st.count) : 0,
            lastUsed: Number.isFinite(Number(st.lastUsed)) ? Number(st.lastUsed) : 0
          };
        }
        function mergeKaomojiStatEntries(entries) {
          let count = 0;
          let lastUsed = 0;
          (entries || []).forEach((st) => {
            count += Number(st && st.count) || 0;
            lastUsed = Math.max(lastUsed, Number(st && st.lastUsed) || 0);
          });
          return { count, lastUsed };
        }
        // 把别名统计折叠进主 key,避免排序只读到“空壳 sk key”而丢掉真正的 lastUsed
        function coalesceKaomojiStatsForOption(stats, opt) {
          const primary = kaomojiKeyFromOption(opt);
          const aliases = listKaomojiStatAliases(opt);
          const merged = mergeKaomojiStatEntries(aliases.map((k) => readKaomojiStatEntry(stats, k)));
          stats[primary] = merged;
          aliases.forEach((k) => {
            if (k !== primary && stats[k]) delete stats[k];
          });
          return merged;
        }
        function getKaomojiStatForOption(stats, opt) {
          const primary = kaomojiKeyFromOption(opt);
          // 已折叠过则直接读主 key;若仍有别名残留则按合并值排序
          if (stats && stats[primary] && typeof stats[primary] === 'object') {
            const aliases = listKaomojiStatAliases(opt);
            const hasAliasResidue = aliases.some((k) => k !== primary && stats[k]);
            if (!hasAliasResidue) return readKaomojiStatEntry(stats, primary);
            return mergeKaomojiStatEntries(aliases.map((k) => readKaomojiStatEntry(stats, k)));
          }
          return mergeKaomojiStatEntries(listKaomojiStatAliases(opt).map((k) => readKaomojiStatEntry(stats, k)));
        }
        function syncKaomojiStatsWithOptions(options) {
          const stats = loadKaomojiStats();
          // 收集当前页面所有颜文字(而非仅当前 select),避免多 select 场景漏合并
          const allOpts = [];
          document.querySelectorAll(SELECTOR).forEach(sel => {
            Array.from(sel.options || []).forEach(opt => {
              const label = (opt?.textContent || '').trim();
              if (!label || label === '无') return;
              allOpts.push(opt);
            });
          });
          (options || []).forEach(opt => {
            const label = (opt?.textContent || '').trim();
            if (!label || label === '无') return;
            if (!allOpts.includes(opt)) allOpts.push(opt);
          });
          allOpts.forEach(opt => {
            const label = (opt?.textContent || '').trim();
            if (!label || label === '无') return;
            coalesceKaomojiStatsForOption(stats, opt);
          });
          // 不主动删除“当前页完全见不到”的孤立 key:扩展颜文字可能晚注入。
          // 但同 option 的别名已在 coalesce 中折叠进主 key。
          saveKaomojiStats(stats);
          return stats;
        }
        function recordKaomojiUsage(opt) {
          const label = (opt?.textContent || '').trim();
          if (!label || label === '无') return;
          const key = kaomojiKeyFromOption(opt);
          const stats = loadKaomojiStats();
          // 先合并历史别名,再累加到主 key,避免“记到 A、排到 B”
          const merged = coalesceKaomojiStatsForOption(stats, opt);
          stats[key] = {
            count: (Number(merged.count) || 0) + 1,
            lastUsed: Date.now()
          };
          saveKaomojiStats(stats);
        }
        function getSortedKaomojiOptions(select) {
          const mode = getSortMode();
          const all = Array.from(select.options || []);
          const base = all
            .filter(opt => (opt.textContent || '').trim() !== '无')
            .map((opt, idx) => ({
              opt,
              idx,
              key: kaomojiKeyFromOption(opt)
            }));
          const stats = syncKaomojiStatsWithOptions(base.map(x => x.opt));
          if (mode === 'freq') {
            base.sort((a, b) => {
              const sa = getKaomojiStatForOption(stats, a.opt);
              const sb = getKaomojiStatForOption(stats, b.opt);
              const dc = (Number(sb.count) || 0) - (Number(sa.count) || 0);
              if (dc !== 0) return dc;
              // 常用模式:频次相同 -> 最近使用优先
              const dt = (Number(sb.lastUsed) || 0) - (Number(sa.lastUsed) || 0);
              if (dt !== 0) return dt;
              // 最近时间也相同(如未使用项 1970) -> 默认顺序
              return a.idx - b.idx;
            });
            return base.map(x => x.opt);
          }
          if (mode === 'recent') {
            const used = [];
            const unused = [];
            base.forEach(x => {
              const st = getKaomojiStatForOption(stats, x.opt);
              if ((Number(st.lastUsed) || 0) > 0) used.push(x);
              else unused.push(x);
            });
            used.sort((a, b) => {
              const sa = getKaomojiStatForOption(stats, a.opt);
              const sb = getKaomojiStatForOption(stats, b.opt);
              const dt = (Number(sb.lastUsed) || 0) - (Number(sa.lastUsed) || 0);
              if (dt !== 0) return dt;
              const dc = (Number(sb.count) || 0) - (Number(sa.count) || 0);
              if (dc !== 0) return dc;
              return a.idx - b.idx;
            });
            // 未使用保持默认顺序(均视作 1970-01-01 00:00:00)
            return used.concat(unused).map(x => x.opt);
          }
          // default:保持原顺序
          return base.map(x => x.opt);
        }
        const selects = document.querySelectorAll(SELECTOR);
        if (!selects.length) return;
        // 注入样式(只注入一次)
        if (!document.getElementById('kaomoji-style')) {
            const style = document.createElement('style');
            style.id = 'kaomoji-style';
            style.textContent = `
                .kaomoji-trigger {
                    display: inline-flex;
                    align-items: center;
                    height: 26px;
                    line-height: 26px;
                    padding: 0 8px;
                    border: 1px solid #ccc;
                    border-radius: 4px;
                    background: #fff;
                    cursor: pointer;
                    white-space: nowrap;
                    user-select: none;
                }
                .kaomoji-panel {
                    position: fixed;
                    z-index: 2147483647;
                    display: none;
                    grid-template-columns: repeat(auto-fill, minmax(${ITEM_W}px, 1fr));
                    gap: ${GAP}px;
                    padding: ${PAD}px;
                    border: 1px solid #ccc;
                    border-radius: 6px;
                    background: #fff;
                    box-shadow: 0 6px 16px rgba(0,0,0,0.12);
                    box-sizing: border-box;
                    overflow-y: auto;
                    /* 关键调整 */
                    width: auto; /* 宽度由 JS 按 qp-quote 动态设置 */
                    max-width: calc(100vw - ${PAD * 2}px); /* 不超过视口宽度 */
                    min-width: ${ITEM_W}px; /* 至少一列 */
                    max-height: calc(${ITEM_H}px * 5 + ${GAP}px * 4 + ${PAD}px * 2);
                }
                .kaomoji-item {
                    width: ${ITEM_W}px;
                    height: ${ITEM_H}px;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    padding: 2px;
                    border-radius: 4px;
                    cursor: pointer;
                    user-select: none;
                    text-align: center;
                    font-size: 14px;
                    line-height: 1.2;
                    word-break: break-word;
                }
                /* 禁用默认 focus 白框/发白 */
                .kaomoji-item:focus {
                  outline: none;
                }
                /* 鼠标悬停:仅变色,不显示蓝框(避免与键盘选中混淆) */
                .kaomoji-item:hover {
                  background: #f2f2f2;
                }
                /* 键盘选中:变色 + 蓝框(专属样式) */
                .kaomoji-item.kaomoji-active {
                  background: #e0e0e0;
                  outline: 2px solid #66ccff;
                  outline-offset: -2px;
                }
            `;
            document.head.appendChild(style);
        }
        selects.forEach(select => {
            if (select.dataset.kaoStyled === '1') return;
            select.dataset.kaoStyled = '1';
            select.style.display = 'none';
            const trigger = document.createElement('button');
            trigger.type = 'button';
            trigger.className = 'kaomoji-trigger';
            trigger.textContent = '选择颜文字';
            // 颜文字按钮右侧快捷排序下拉(复制设置面板的 #sp_kaomojiSort)
            const quickSort = document.createElement('select');
            quickSort.className = 'sp_kaomojiSort_copy';
            quickSort.setAttribute('data-copy-of', 'sp_kaomojiSort');
            quickSort.style.height = '26px';
            quickSort.style.marginLeft = '6px';
            quickSort.style.flex = '0 0 auto';
            // 复制设置面板中的下拉项;若面板未渲染则使用同款兜底项
            const panelSort = document.querySelector('#sp_kaomojiSort');
            if (panelSort && panelSort.options && panelSort.options.length) {
              Array.from(panelSort.options).forEach(op => {
                quickSort.appendChild(new Option(op.textContent, op.value));
              });
            } else {
              quickSort.appendChild(new Option('默认', 'default'));
              quickSort.appendChild(new Option('最近', 'recent'));
              quickSort.appendChild(new Option('常用', 'freq'));
            }
            quickSort.value = getSortMode();
            const panel = document.createElement('div');
            panel.className = 'kaomoji-panel';
            //panel.tabIndex = -1; // 👈 添加:使 panel 可以接收焦点
            // 复制颜文字:右键菜单与键盘 C 共用;成功/失败反馈一致,焦点回到对应文本框
            function copyKaomojiValue(value) {
              const textarea = (select && select.closest && select.closest('form'))
                ? select.closest('form').querySelector('textarea.h-post-form-textarea[name="content"]')
                : null;
              const restoreFocus = () => {
                try {
                  if (textarea && typeof textarea.focus === 'function') textarea.focus();
                } catch (e) {}
              };
              return writeClipboardText(value, null)
                .then(() => {
                  toast('颜文字已复制', 900, { queue: false, key: 'kaomoji-copy' });
                  hidePanel();
                  restoreFocus();
                })
                .catch((err) => {
                  console.warn('[kaomoji] copy failed:', err);
                  toast('颜文字复制失败', 900, { queue: false, key: 'kaomoji-copy' });
                  restoreFocus();
                });
            }
            function renderPanelItems() {
              while (panel.firstChild) panel.removeChild(panel.firstChild);
              const options = getSortedKaomojiOptions(select);
              options.forEach(opt => {
                const item = document.createElement('div');
                item.className = 'kaomoji-item';
                item.textContent = opt.textContent;
                item.dataset.value = opt.value;
                item.tabIndex = -1; // 使 item 可聚焦,避免 focus 时“发白/丢焦点”
                item.addEventListener('click', () => {
                  recordKaomojiUsage(opt); // 无论是否开启排序都记录
                  select.value = opt.value;
                  select.dispatchEvent(new Event('change', { bubbles: true }));
                  trigger.textContent = opt.textContent || '选择颜文字';
                  hidePanel();
                });
                item.addEventListener('contextmenu', (e) => {
                  e.preventDefault();
                  e.stopPropagation();
                  copyKaomojiValue(opt.value);
                });
                panel.appendChild(item);
              });
            }
            renderPanelItems();
            select.parentNode.insertBefore(trigger, select.nextSibling);
            trigger.insertAdjacentElement('afterend', quickSort);
            // 默认位置(非 qp-quote)下,uk-width-1-5 会导致换行:扩展输入容器并强制同排
            const inputCell = select.parentElement;
            if (inputCell) {
              Object.assign(inputCell.style, {
                display: 'flex',
                alignItems: 'center',
                flexWrap: 'nowrap',
                minWidth: '0',
                gap: '6px'
              });
              trigger.style.flex = '0 0 auto';
              if (!trigger.closest('.qp-quote')) {
                inputCell.classList.remove('uk-width-1-5');
                inputCell.style.width = 'auto';
                inputCell.style.maxWidth = 'none';
                inputCell.style.flex = '1 1 auto';
              }
            }
            document.body.appendChild(panel);
            function getQuoteElement() {
              // 仅当当前触发器本身位于回复面板中时,才跟随 qp-quote 定位
              // 避免默认页面场景误命中页面上其他/隐藏的 qp-quote,导致面板贴边
              return trigger.closest('.qp-quote');
            }
            function getPanelAnchorRect() {
              const quoteEl = getQuoteElement();
              if (quoteEl) return quoteEl.getBoundingClientRect();
              return trigger.getBoundingClientRect();
            }
            function getPanelTargetWidth() {
              const quoteEl = getQuoteElement();
              if (quoteEl) {
                const w = Math.round(quoteEl.getBoundingClientRect().width || 0);
                if (w > 0) return Math.max(ITEM_W, w);
              }
              // 未打开回复面板时,使用与回复面板一致的初始宽度:min(90vw, 820px)
              // (与 replaceRightSidebar 中 .qp-stack 的初始宽度保持一致)
              const replyInitW = Math.round(Math.min(window.innerWidth * 0.9, 820));
              return Math.max(ITEM_W, replyInitW);
            }
            function positionPanel() {
              const triggerRect = trigger.getBoundingClientRect();
              const quoteEl = getQuoteElement();
              const anchorRect = getPanelAnchorRect();
              const margin = 6;
              // 宽度跟随 qp-quote,并限制在可视区域内(窗口变窄时同步变窄)
              const targetW = getPanelTargetWidth();
              const maxW = Math.max(ITEM_W, window.innerWidth - margin * 2);
              const finalW = Math.min(targetW, maxW);
              // 写入会脏化布局,紧随其后的 getBoundingClientRect 就退化成强制同步重排;值未变则不写
              const nextW = `${Math.round(finalW)}px`;
              if (panel.style.width !== nextW) panel.style.width = nextW;
              // 若当前不可见,临时显示用于测量
              const wasHidden = (panel.style.display === 'none' || panel.style.display === '');
              if (wasHidden) {
                panel.style.display = 'grid';
                panel.style.visibility = 'hidden';
              }
              const panelRect = panel.getBoundingClientRect();
              const panelW = panelRect.width;
              const panelH = panelRect.height;
              // 横向:有 qp-quote 时跟随其左边缘;无 qp-quote 时居中
              let left = quoteEl ? anchorRect.left : (window.innerWidth - panelW) / 2;
              let top = triggerRect.top - panelH - 6;
              if (left + panelW > window.innerWidth - margin) {
                  left = window.innerWidth - margin - panelW;
              }
              if (left < margin) left = margin;
              if (top < margin) {
                  top = triggerRect.bottom + 6;
              }
              if (top + panelH > window.innerHeight - margin) {
                  top = Math.max(margin, window.innerHeight - margin - panelH);
              }
              const nextLeft = `${Math.round(left)}px`;
              const nextTop = `${Math.round(top)}px`;
              if (panel.style.left !== nextLeft) panel.style.left = nextLeft;
              if (panel.style.top !== nextTop) panel.style.top = nextTop;
              if (wasHidden) {
                panel.style.visibility = '';
              }
          }
          let followRafId = 0;
          let followResizeHandler = null;
          let followScrollHandler = null;
          function startPanelFollow() {
            stopPanelFollow();
            followResizeHandler = () => {
              if (panel.style.display === 'grid') positionPanel();
            };
            followScrollHandler = () => {
              if (panel.style.display === 'grid') positionPanel();
            };
            window.addEventListener('resize', followResizeHandler);
            // 捕获阶段监听,兼容任意滚动容器
            window.addEventListener('scroll', followScrollHandler, true);
            const tick = () => {
              if (panel.style.display !== 'grid') {
                followRafId = 0;
                return;
              }
              positionPanel();
              followRafId = window.requestAnimationFrame(tick);
            };
            followRafId = window.requestAnimationFrame(tick);
          }
          function stopPanelFollow() {
            if (followRafId) {
              window.cancelAnimationFrame(followRafId);
              followRafId = 0;
            }
            if (followResizeHandler) {
              window.removeEventListener('resize', followResizeHandler);
              followResizeHandler = null;
            }
            if (followScrollHandler) {
              window.removeEventListener('scroll', followScrollHandler, true);
              followScrollHandler = null;
            }
          }
          function showPanel() {
            // 每次打开都按最新统计与排序模式重建,确保“关闭后再开”顺序实时更新
            renderPanelItems();
            positionPanel();
            panel.style.display = 'grid';
            panel.style.visibility = '';
            panel.scrollTop = 0;
            bindGlobalClose();
            startPanelFollow();
            initKeyboardNav();
            // 👇 添加:延迟聚焦,确保 DOM 渲染完成
            setTimeout(() => {
                const items = panel.querySelectorAll('.kaomoji-item');
                if (items.length > 0) {
                    items[0].focus();
                }
            }, 0);
          }
          function hidePanel() {
            panel.style.display = 'none';
            unbindGlobalClose();
            stopPanelFollow();
            removeKeyboardNav();
          }
            // 点击快捷排序前先关闭颜文字面板,避免用户眼前发生重排
            quickSort.addEventListener('mousedown', () => {
              if (panel.style.display === 'grid') hidePanel();
            }, true);
            quickSort.addEventListener('change', () => {
              const mode = quickSort.value || 'default';
              try {
                if (SettingPanel && SettingPanel.state) SettingPanel.state.kaomojiSort = mode;
              } catch (e) {}
              try {
                const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
                cfg.kaomojiSort = mode;
                GM_setValue(SettingPanel.key, cfg);
              } catch (e) {}
              // 与设置面板中的下拉同步
              const panelSort = document.querySelector('#sp_kaomojiSort');
              if (panelSort) panelSort.value = mode;
              // 与其他快捷下拉同步
              document.querySelectorAll('.sp_kaomojiSort_copy').forEach(el => {
                if (el !== quickSort) el.value = mode;
              });
              // 即时生效
              document.querySelectorAll(SELECTOR).forEach(sel => {
                try {
                  sel.dispatchEvent(new Event('kaomoji:sort-changed'));
                } catch (e) {}
              });
              // 广播模式变化(供后续新建的复制下拉同步)
              try {
                window.dispatchEvent(new CustomEvent('kaomoji:sort-mode-changed', { detail: { mode, source: 'quick' } }));
              } catch (e) {}
            });
            const onSortModeChanged = (ev) => {
              const mode = ev?.detail?.mode || getSortMode();
              if (quickSort.value !== mode) quickSort.value = mode;
            };
            window.addEventListener('kaomoji:sort-mode-changed', onSortModeChanged);
            trigger.addEventListener('click', (e) => {
                e.stopPropagation();
                if (panel.style.display === 'none' || panel.style.display === '') {
                    showPanel();
                } else {
                    hidePanel();
                }
            });
          let outsideHandler, escHandler;
          function bindGlobalClose() {
              outsideHandler = (e) => {
                // 👇 忽略由键盘触发的伪鼠标事件 
                if (e.pointerType === '' || e.detail === 0) { 
                  return; 
                }
                // 捕获阶段执行,防止被其他脚本阻止
                const target = e.target;
                if (!panel.contains(target) && target !== trigger) {
                    hidePanel();
                }
              };
              escHandler = (e) => {
                if (e.key === 'Escape') {
                    e.preventDefault();
                    hidePanel();
                }
            };
              // 用捕获阶段监听 click 和 mousedown
              document.addEventListener('click', outsideHandler, true);
              document.addEventListener('mousedown', outsideHandler, true);
              window.addEventListener('keydown', escHandler);
          }
          function unbindGlobalClose() {
            document.removeEventListener('click', outsideHandler, true);
            document.removeEventListener('mousedown', outsideHandler, true);
            window.removeEventListener('keydown', escHandler);
          }
          let currentIndex = -1;
          let keyboardHandler;
          //let highlightedItems = new Set();
          function clearActive(items) {
            items.forEach(item => item.classList.remove('kaomoji-active'));
          }
          function setActive(items, index) {
            if (!items.length) return;
            const next = Math.max(0, Math.min(index, items.length - 1));
            if (currentIndex === next && items[currentIndex] && items[currentIndex].classList.contains('kaomoji-active')) {
              return;
            }
            if (currentIndex >= 0 && items[currentIndex]) {
              items[currentIndex].classList.remove('kaomoji-active');
            }
            currentIndex = next;
            const el = items[currentIndex];
            el.classList.add('kaomoji-active');
            el.focus({ preventScroll: true });
            el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
          }
          function initKeyboardNav() {
            const items = Array.from(panel.querySelectorAll('.kaomoji-item'));
            if (!items.length) return;
            // 重置状态
            clearActive(items);
            currentIndex = -1;
            setActive(items, 0);
            keyboardHandler = (e) => {
                // 如果面板不可见,直接返回
                if (panel.style.display === 'none' || panel.style.display === '') {
                  return;
                }
                const items = Array.from(panel.querySelectorAll('.kaomoji-item'));
                if (!items.length) return;
                // const panelWidth = panel.offsetWidth - PAD * 2;
                // const cols = Math.floor(panelWidth / (ITEM_W + GAP));
                // // 使用实际 grid 列数,避免计算误差
                // const gridCols = getComputedStyle(panel).gridTemplateColumns.split(/\s+/).filter(s => s && s !== '').length;
                // const cols = gridCols > 0 ? gridCols : 1;
                // 通过实际位置计算列数
                // let cols = items.length; // 默认只有一行
                // if (items.length > 1) {
                //     const firstTop = items[0].getBoundingClientRect().top;
                //     for (let i = 1; i < items.length; i++) {
                //         if (items[i].getBoundingClientRect().top > firstTop) {
                //             cols = i;
                //             break;
                //         }
                //     }
                // }
                // 通过首行实际 offsetTop 统计列数,避免两行颜文字导致行高变化影响判定
                let cols = 1; // 默认至少一列
                if (items.length > 1) {
                  const firstTop = items[0].offsetTop;
                  let count = 1;
                  for (let i = 1; i < items.length; i++) {
                    if (Math.abs(items[i].offsetTop - firstTop) <= 1) {
                      count++;
                    } else {
                      break;
                    }
                  }
                  cols = Math.max(1, count);
                }
                let newIndex = currentIndex;
                const key = e.key.toLowerCase();
                if (e.key === 'ArrowRight' || key === 'd') {
                    e.preventDefault();
                    newIndex = Math.min(currentIndex + 1, items.length - 1);
                } else if (e.key === 'ArrowLeft' || key === 'a') {
                    e.preventDefault();
                    newIndex = Math.max(currentIndex - 1, 0);
                } else if (e.key === 'ArrowDown' || key === 's') {
                    e.preventDefault();
                    newIndex = Math.min(currentIndex + cols, items.length - 1);
                } else if (e.key === 'ArrowUp' || key === 'w') {
                    e.preventDefault();
                    newIndex = Math.max(currentIndex - cols, 0);
                } else if (e.key === 'Enter' || key === ' ') {
                    e.preventDefault();
                    items[currentIndex].click();
                    return;
                } else if (key === 'c' && !e.ctrlKey && !e.metaKey && !e.altKey) {
                    // 焦点在面板内时,按 C 复制当前高亮颜文字(与右键复制一致)
                    const activeEl = document.activeElement;
                    if (panel.contains(activeEl) && items[currentIndex] && items[currentIndex].dataset && items[currentIndex].dataset.value) {
                        e.preventDefault();
                        copyKaomojiValue(items[currentIndex].dataset.value);
                    }
                    return;
                }
                if (newIndex !== currentIndex) {
                  setActive(items, newIndex);
                }
            };
            window.addEventListener('keydown', keyboardHandler);
          }
          function removeKeyboardNav() {
            if (keyboardHandler) {
                window.removeEventListener('keydown', keyboardHandler);
                keyboardHandler = null;
            }
            // 清除所有颜文字的高亮样式
            const items = panel.querySelectorAll('.kaomoji-item');
            items.forEach(item => item.classList.remove('kaomoji-active'));
            currentIndex = -1;
          }
          select.addEventListener('kaomoji:updated', () => {
            renderPanelItems();
          });
          select.addEventListener('kaomoji:sort-changed', () => {
            renderPanelItems();
            if (panel.style.display === 'grid') {
              initKeyboardNav();
            }
          });
      });
    }
      /**
       * 功能 3:颜文字样式拓展
       */
      function extendKaomojiSet() {
          const EXTRA_EMOTS = [
              "( ´_ゝ`)旦","(<ゝω・) ☆","(`ε´ (つ*⊂)","=͟͟͞͞( 'ヮ' 三 'ヮ' =͟͟͞͞)","↙(`ヮ´ )↗ 开摆!",
              "(っ˘Д˘)ノ<","(ノ#)`д´)σ","₍₍(ง`ᝫ´ )ว⁾","( `ᵂ´)","( *・ω・)✄╰ひ╯","U•ェ•*U","⊂( ゚ω゚)つ",
              "( ゚∀。)7","・゚( ゚∀。) ゚。","\\( ゚∀。)/","(╬゚∀。)","( `д´)σ","( ゚ᯅ 。)","( ;`д´; )","m9( `д´)","( ゚π。)","ᕕ( ゚∀。)ᕗ","ᕙ( ゚∀。)ᕗ",
              "ฅ(^ω^ฅ)","(|||^ヮ^)","(|||ˇヮˇ)","( ↺ω↺)"," `ー´) `д´) `д´)",
              "₍˄·͈༝·͈˄₎◞","⁽ ˇᐜˇ⁾","⁽ ˆ꒳ˆ⁾","⁽ ^ᐜ^⁾","⁽´°`⁾","⁽´ᵖ`⁾","⁽ ˙³˙⁾","⁽°ᵛ°⁾","⁽ `ᵂ´⁾",
              "( ;ˇωˇ; )","( ‸ო‸)","( ‸ω‸)","( ^ω。)","/̵͇̿̿/’̿’̿ ̿ ̿̿ ̿̿ ̿̿","( ;´ω`)人 ","_(:зゝ∠)_","( ゚ 灬゚)","( `д´)7","(ᗜᴗᗜ)","(~゚ω゚)~",
              "接☆龙☆大☆成☆功","ᑭ`д´)ᓀ ∑ᑭ(`ヮ´ )ᑫ","乚 (^ω^ ミэ)Э好钩我咬","乚(`ヮ´  ミэ)Э","( ゚∀。ミэ)Э三三三三 乚",
              "(ˇωˇ ミэ)Э三三三三 乚","(‸ω‸ ミэ)Э","( へ ゚∀゚)べ摔低低","(ベ ˇωˇ)べ 摔低低",
          ];
          const EXTRA_RICH = {
              "҉( ゚∀。)": "  ҉\n( ゚∀。)",
              "⬭꒰ঌ( ˇωˇ)໒꒱":"  ⬭\n꒰ঌ( ˇωˇ)໒꒱",
              "齐齐蛤尔": "(`ヮ´ )σ`∀´) ゚∀゚)σ",
              "呼伦悲尔": "( ノд`゚);´д`) ´_ゝ`)",
              "愕尔多厮": "Σ( ゚д゚)´゚Д゚) ゚д゚)))",
              "智利": "( ゚∀。)∀。)∀。)",
              "阴山山脉": "( ˇωˇ )◕∀◕。)^ω^)",
              "mini阴阳酱":"₍˄·͈༝·͈˄₎◞⁽ ˇᐜˇ⁾⁽ ˆ꒳ˆ⁾⁽ ^ᐜ^⁾⁽´°`⁾⁽´ᵖ`⁾⁽ ˙³˙⁾⁽°ᵛ°⁾⁽ `ᵂ´⁾",
              "F5欧拉": " σ σ\nσ( ´ρ`)σ[F5]\n σ σ",
              "UK酱": "\\ ︵\nᐕ)⁾⁾",
              "白羊": "╭◜◝ ͡ ◜◝ J J\n(     `д´)  “咩!”\n╰◟д ◞ ͜ ◟д◞",
              "兔兔": " /) /)\nc( ╹^╹)",
              "neko": "     ∧,, \n    ヾ `. 、`フ\n   (,`'´ヽ、、ヅ\n  (ヽv'   `''゙つ\n  ,ゝ  ⌒`y'''´\n  ( (´^ヽこつ\n   ) )\n  (ノ",
              "neko2": "   、ゞヾ∧\"\"'∧;,\n  ヾ   ・ω・ 彡\n  ミ     o  o  ミ\n ~彡 ミ\n  /ソ,, ,0; ,;;:、0ヾ`",
              "neko3": "     /l、 \n(*´∀`)σ(゚∀。7\n     l、 ~ヽ\n    じしf_, )ノ ​",
              "给你": "(\\_/)\n(・_・)\n /  >",
              "催更喵gkd": "      ___\n     />  フ\n     |  _  _ l 我是一只催更的\n     /` ミ_xノ 喵喵酱\n     /      | gkdgkd\n    /  ヽ   ノ\n    │  | | |\n / ̄|   | | |\n | ( ̄ヽ__ヽ_)__)\n \二つ",
              "举高高": "    _∧_∧_ \n    ((∀`/  ) \n   /⌒   / \n  /(__ノ\_ノ \n  (_ノ ||| 举高高~~\n ∧_∧ ∧_∧\n(( ・∀・ ))・∀・) )\n`\   ∧   ノ\n / |/  |\n(_ノ_)_ノL_)",
              "举高高2": "    _∧_∧_\n    ((∀`/  )\n    /⌒   /\n  /(__ノ\_ノ(((ヽ\n (_ノ    ̄Y\\n| (\ (\\  /)  | )举高高!\nヽ ヽ` ( ゚∀゚ ) _ノ /\n \ | ⌒Y⌒ / /\n  |ヽ · | · ノ //\n  \トー仝ーイ\n   | ミ土彡/\n   ). \\ °  /\n   (  \\. y  \\",
              "举糕糕": "举糕糕~\n  ☆☆☆☆☆☆☆☆\n ╭┻┻┻┻┻┻┻┻╮\n ┃╱╲╱╲╱╲╱╲┃\n╭┻━━━━━━━━┻╮\n┃╱╲╱╲╱╲╱╲╱╲┃\n┗━━━━━━━━━━┛\n   ∧_∧ ∧_∧\n  (( ・∀・ ))・∀・) )\n  `\   ∧   ノ\n   /  |/  |\n  (_ノ_)_ノL_)",
              "Happy肥肥Day": ".       .★ * ★.. \n  .*★ *. *..*   ★ \n  ★      ★ \n  ‘*. *'  ʜᴀᴘᴘʏ 肥肥 ᴅᴀʏ \n   ‘★.   ★’ \n     *..★\n┊┊┊┊☆☆☆☆☆☆☆☆┊┊┊┊\n┊┊┊╭┻┻┻┻┻┻┻┻╮┊┊┊\n┊┊┊┃╱╲╱╲╱╲╱╲┃┊┊┊\n┊┊╭┻━━━━━━━━┻╮┊┊\n┊┊┃╱╲╱╲╱╲╱╲╱╲┃┊┊\n╱▔┗━━━━━━━━━━┛▔╲",
              "大嘘": "吁~~~~  rnm,退钱!\n    /   /\n( ゚ 3゚) `ー´) `д´) `д´)",
              "巴拉巴拉": " ∧_∧\n(。・ω・。)つ━☆・*。\n⊂   ノ    ・゜+.\n しーJ   °。+ *´¨)\n      .· ´¸.·*´¨) ¸.·*¨)\n         (¸.·´ (¸.·’*",
              "碣石": "  _ _\n  ( ゚_゚)  \n/ (⁰  )╲/",
              "冰封王座": "(ノ゚∀゚)ノ👑\n   ( ゚∀。)\n\n   👑\n(//゚ω゚)//\n    \n 👑\n(Ⅱ゚ω゚)Ⅱ\n\n  👑\nᕕ( ᐛ )ᕗ",
              "冰封王座2": "(ノ゚∀゚)ノ🍟\n   ( ゚∀。)\n\n   🍟\n(//゚ω゚)//\n    \n 🍟\n(Ⅱ゚ω゚)Ⅱ\n\n  🍟\nᕕ( ᐛ )ᕗ",
              "冰封王座3": "( `д´)=🔪 👑\n    ( ゚ 3゚)\n   👑\n(// `ー´)//\n( x 3x)\n 👑\n(Ⅱ`∀´)Ⅱ\n( x 3x)\n 👑\n( `д´)\n( x 3x)",
              "喵喵酱": " ^ ^\n( =`д´=)哈—!",
              "狗比酱": "(U `д´)<汪汪汪!",
              "狼酱":"     ႔ ႔\n嗷呜!> ᠸ`д´ 𐅠",
              "起舞":"⊂ヽ( ^ω^)つ\n  \   /  \n  ( __フ\n  (/",
              "N98": "淦\n是\nN\n9\n8\n接☆龙☆大☆成☆功\n盗摄?( ^ω^)\n石雕\n要素齐全\nN98是什么?\n同问\n感谢\n谢谢\n要素齐全\n什么是要素齐全?\n淦\n是\nN\n9\n5\nr\n警惕r点机器人\n你一个人都说完了我们说什么(╬゚д゚)",
              "望po石":" ┏━┓\n ┃望┃\nᕕ┃po┃ᕗ\n ┃石┃\n ┗━┛\n嗨呀我又来望po了\n我天天都来这望po",
              "望po石2": "    ┏━┓\n    ┃望┃\n    ┃po┃\n    ┃石┃\n    ┗━┛    \n   ┏━━━┓\n  ┏┛ 望 ┗┓\n ┏┛  po  ┗┓\n┏┛   山   ┗┓\n┗━━━━━━━━━┛",
              "撞墙": "┃電柱┃ ( ´ー`)\n┃電柱┃д゚ ) =͟͟͞͞ =͟͟͞͞\n┃電柱┃ ( ´д`)\n┃電柱┃д゚ ) =͟͟͞͞ =͟͟͞͞\n┃電柱┃ (;´Д`)\n┃電柱┃π。) =͟͟͞͞ =͟͟͞͞",
              "冰箱先生":"  /\n┌───┐\n│ ゚∀゚│\n├─┬─┤\n│ │ │\n└─┴─┘",
              "冰箱先生3D":"  /\n▁▁▁▁\n╲   ╲\n▏┌───┐\n▏│ ゚∀゚ │\n▏├─┬─┤\n╲│ │ │\n └─┴─┘",
              "血压↑":" 他妈的/>__フ\n    | = ^ω^)= 血压↗↗\n    /`   丨",
              "血压0↓":" 算逑喽/>__フ\n    | = ˇωˇ)= 血压0↓\n    /`   丨",
              "挺好颜":"╯  乀\nヘ  へ\n ′\n ﹀",
              "全角空格": " ",
              "零宽空格": "​",
          };
          const ORDERED_RICH = [
              "҉( ゚∀。)","⬭꒰ঌ( ˇωˇ)໒꒱","齐齐蛤尔","呼伦悲尔","愕尔多厮","智利","阴山山脉",
              "mini阴阳酱","F5欧拉","UK酱","白羊","兔兔",
              "neko","neko2","neko3","催更喵gkd",
              "给你","举高高","举高高2","举糕糕","Happy肥肥Day",
              "大嘘","巴拉巴拉","碣石",
              "冰封王座","冰封王座2","冰封王座3",
              "喵喵酱","狗比酱","狼酱","起舞","N98",
              "望po石","望po石2","撞墙","冰箱先生","冰箱先生3D","血压↑","血压0↓","挺好颜",
              "全角空格","零宽空格",
              // 页面中“防剧透/骰子/高级骰子”不动其原位
          ];
          const NEED_LF = new Set([
              "҉( ゚∀。)","⬭꒰ঌ( ˇωˇ)໒꒱","F5欧拉","UK酱","白羊","兔兔",
              "neko","neko2","neko3","催更喵gkd",
              "给你","举高高","举高高2","举糕糕","Happy肥肥Day",
              "大嘘","巴拉巴拉","碣石",
              "冰封王座","冰封王座2","冰封王座3",
              "喵喵酱","狗比酱","狼酱","起舞","N98",
              "望po石","望po石2","撞墙","冰箱先生","冰箱先生3D","血压↑","血压0↓","挺好颜",
          ]);
          // 一次性补齐(选择器就绪且已有至少一个选项时调用)
          function patchSelect(sel) {
              if (!sel || sel.dataset.kaoExtended === '1') return;
              if (!sel.options || sel.options.length === 0) return;
              // 去重集合
              const existingValues = new Set();
              const existingLabels = new Set();
              Array.from(sel.options).forEach(op => {
                  existingValues.add(op.value);
                  existingLabels.add(op.textContent);
              });
              // 追加普通
              const frag = document.createDocumentFragment();
              EXTRA_EMOTS.forEach(txt => {
                  if (!existingValues.has(txt)) {
                    const op = new Option(txt, txt);
                    op.dataset.kaoKey = `extra:${txt}`;
                    frag.appendChild(op);
                      existingValues.add(txt);
                  }
              });
              if (frag.childNodes.length) sel.appendChild(frag);
              // 收集需要重排的富颜文字:先把 ORDERED_RICH 中已存在的挪出
              const bucket = new Map(); ORDERED_RICH.forEach(k => bucket.set(k, null));
              for (let i = sel.options.length - 1; i >= 0; i--) {
                  const op = sel.options[i];
                  const label = op.textContent;
                  if (bucket.has(label)) {
                      bucket.set(label, op);
                      sel.remove(i);
                  }
              }
              // 按顺序插回;缺的用 EXTRA_RICH 补齐;值前加换行(NEED_LF)
              const richFrag = document.createDocumentFragment();
              ORDERED_RICH.forEach(key => {
                  let node = bucket.get(key);
                  if (node) {
                    node.dataset.kaoKey = `rich:${key}`;
                      richFrag.appendChild(node);
                  } else if (EXTRA_RICH[key]) {
                      const val = NEED_LF.has(key) ? ("\n" + EXTRA_RICH[key] + "\n") : EXTRA_RICH[key];
                    const op = new Option(key, val);
                    op.dataset.kaoKey = `rich:${key}`;
                    richFrag.appendChild(op);
                  }
              });
              if (richFrag.childNodes.length) sel.appendChild(richFrag);
              sel.dataset.kaoExtended = '1';
              sel.dispatchEvent(new CustomEvent('kaomoji:updated', { bubbles: true }));
          }
          function patchSelectsIn(root = document) {
              if (!root) return;
              const selects = [];
              if (root.matches && root.matches(SELECTOR)) selects.push(root);
              if (root.querySelectorAll) root.querySelectorAll(SELECTOR).forEach(sel => selects.push(sel));
              selects.forEach(sel => {
                  if (sel.options && sel.options.length > 0) patchSelect(sel);
              });
          }
          // 方案 A:钩住 jQuery 的 append,仅针对 #h-emot-select
          (function hookjQueryAppend(){
              const $ = window.jQuery;
              if (!$ || !$.fn || !$.fn.append || $.fn.append.__kaoHooked) return;
              const rawAppend = $.fn.append;
              $.fn.append = function(...args){
                  const ret = rawAppend.apply(this, args);
                  try {
                      // 如果目标包含我们的 select,就尝试打补丁
                      this.each(function(){
                          patchSelectsIn(this);
                      });
                  } catch(_) {}
                  return ret;
              };
              $.fn.append.__kaoHooked = true;
          })();
          // 方案 B:用 MutationObserver 监听 select 的子节点变化,首次填充后补齐
          (function observeSelect(){
              document.querySelectorAll(SELECTOR).forEach(sel => {
                  if (sel.options && sel.options.length > 0) {
                      patchSelect(sel);
                      return;
                  }
                  if (sel.dataset.kaoOptionObserver === '1') return;
                  sel.dataset.kaoOptionObserver = '1';
                  const mo = new MutationObserver(() => {
                      if (sel.options && sel.options.length > 0) {
                          patchSelect(sel);
                          mo.disconnect();
                          delete sel.dataset.kaoOptionObserver;
                          scheduleKaomojiEnhancePass();
                      }
                  });
                  mo.observe(sel, { childList: true, subtree: false });
              });
          })();
          // 方案 C:兜底重试(避免异步加载错过时机)
          let tries = 0;
          (function retry(){
              patchSelectsIn(document);
              const pending = Array.from(document.querySelectorAll(SELECTOR)).some(sel => !sel.dataset.kaoExtended);
              if (!pending) return;
              if (tries++ < 30) {
                  setTimeout(retry, 100);
              }
          })();
      }
  }
  // function renderSpoiler(container) {
  //   var $container = $(container);
  //   // 用构造函数写法,避免 /^.../ 形式
  //   var reg = new RegExp("\\[h\\]([\\s\\S]*?)\\[\\/h\\]", "gi");

  //   $container.find('.h-threads-content').each(function () {
  //     var text = $(this).text().trim();
  //     var match = reg.exec(text);
  //     if (match) {
  //       $(this).html('<span class="h-hidden-text">' + match[1] + '</span>');
  //     }
  //   });
  // }

  /* --------------------------------------------------
   * tag 14. ‘增强x岛匿名版’:添加预览框+草稿保存/恢复/自动设置网页标题/人类友好时间显示/引用追记/粘贴图片上传
   * -------------------------------------------------- */
  // 统一生成草稿存储用的 key
  // 统一:草稿 key 生成
  // ★ 草稿键规范化:草稿是「串级」共享数据,同一串在任何页面/任何页码回复都读写同一槽。
  //   /t/{tid}、/t/{tid}/page/N、/Forum/po/id/{tid}/... → 统一为 '/t/{tid}'
  //   非串内页(/f/xxx 板块页、时间线、搜索页等)→ 返回空串,草稿功能整体短路
  //   (只有串内才有草稿:板块页发串表单不保存/不载入草稿)
  function getDraftKey() {
    return normalizeDraftKeyFromPath(window.location.pathname);
  }
  function normalizeDraftKeyFromPath(pathname) {
    const path = String(pathname || '');
    const parsed = parseThreadHistoryUrl(path);
    if (!parsed || !parsed.threadId) return '';
    return '/t/' + parsed.threadId;
  }
  function getDraftStorageKey(pathname = getDraftKey()) {
    return `xdex_draft:${pathname}`;
  }
  function getDraftRegistryKey() {
    return 'xdex_draft_registry';
  }
  function isLegacyDraftKey(key) {
    return /^\/t\/\d+$/.test(key) || /^\/f\/[^/]+$/.test(key) || /^\/Forum\/timeline\/id\/\d+$/.test(key);
  }
  function getDraftRegistry() {
    try {
      if (typeof GM_getValue === 'function') {
        const value = GM_getValue(getDraftRegistryKey(), []);
        if (!Array.isArray(value)) return [];
        // 惰性自愈: 值为空的内容不保留——避免旧版本留存的空草稿键导致存储膨胀
        const clean = value.filter((key) => {
          try {
            const v = (typeof GM_getValue === 'function') ? GM_getValue(key, '') : '';
            return typeof v === 'string' && v !== '';
          } catch (_) { return false; }
        });
        if (clean.length !== value.length) {
          try { GM_setValue(getDraftRegistryKey(), clean); } catch (_) {}
          value.forEach((key) => {
            if (clean.indexOf(key) === -1) {
              try { if (typeof GM_deleteValue === 'function') GM_deleteValue(key); else GM_setValue(key, ''); } catch (_) {}
            }
          });
        }
        return clean;
      }
    } catch (_) {}
    return [];
  }
  function saveDraftRegistry(list) {
    try {
      if (typeof GM_setValue === 'function') {
        GM_setValue(getDraftRegistryKey(), Array.from(new Set(Array.isArray(list) ? list : [])));
      }
    } catch (_) {}
  }
  function addDraftKeyToRegistry(storageKey) {
    if (!storageKey) return;
    const registry = getDraftRegistry();
    if (!registry.includes(storageKey)) {
      registry.push(storageKey);
      saveDraftRegistry(registry);
    }
  }
  function removeDraftKeyFromRegistry(storageKey) {
    if (!storageKey) return;
    const next = getDraftRegistry().filter((key) => key !== storageKey);
    saveDraftRegistry(next);
  }
  function getDraftEnabledNow() {
    try {
      if (typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state) {
        return !!SettingPanel.state.enableDraft;
      }
      if (typeof GM_getValue === 'function') {
        const saved = GM_getValue(SettingPanel.key, {});
        return !!Object.assign({}, SettingPanel.defaults, saved).enableDraft;
      }
    } catch (_) {}
    return true;
  }
  function getLegacyDraftValue(pathname = getDraftKey()) {
    try {
      if (typeof GM_getValue === 'function') {
        const value = GM_getValue(pathname, '');
        return typeof value === 'string' ? value : '';
      }
    } catch (_) {}
    return '';
  }
  function readDraftValue(pathname = getDraftKey()) {
    const storageKey = getDraftStorageKey(pathname);
    let value = '';
    try {
      if (typeof GM_getValue === 'function') {
        value = GM_getValue(storageKey, '');
      }
    } catch (_) {}
    if (typeof value === 'string' && value !== '') return value;
    return getLegacyDraftValue(pathname);
  }
  function saveDraftValue(pathname, content) {
    if (!getDraftEnabledNow()) return;
    const storageKey = getDraftStorageKey(pathname);
    // 正文清空等于删除草稿: 删除本地并登记账本, 防止云端旧草稿经同步复活
    if (String(content == null ? '' : content).trim() === '') {
      deleteDraftSafe(storageKey);
      return;
    }
    try {
      if (typeof GM_setValue === 'function') {
        GM_setValue(storageKey, content);
        addDraftKeyToRegistry(storageKey);
        removeDraftDeletion(storageKey);   // 重新编辑即翻案: 撤销该 URL 的删除标记
      }
    } catch (_) {}
  }
  function migrateLegacyDraftIfNeeded(pathname = getDraftKey()) {
    const legacyValue = getLegacyDraftValue(pathname);
    if (typeof legacyValue !== 'string' || legacyValue === '') return '';
    const storageKey = getDraftStorageKey(pathname);
    let current = '';
    try {
      if (typeof GM_getValue === 'function') {
        current = GM_getValue(storageKey, '');
      }
    } catch (_) {}
    if (!current) {
      try {
        if (typeof GM_setValue === 'function') {
          GM_setValue(storageKey, legacyValue);
          addDraftKeyToRegistry(storageKey);
        }
      } catch (_) {}
    }
    deleteDraftSafe(pathname);
    return legacyValue;
  }
  // ★ 草稿槽规范化迁移:历史版本按 location.pathname 存草稿,同一串产生多个槽
  //(/t/{tid}、/t/{tid}/page/N、/Forum/po/id/{tid}/page/N 各一份)。本函数把
  // 所有旧槽合并进规范槽 '/t/{tid}':
  //  - 规范槽已有内容 → 保留;否则取「registry 中位置最靠后」(最近首次保存)的非空旧槽
  //  - 被清理的旧槽:GM_deleteValue + 登记删除账本(防 WebDAV 同步复活)
  //  - 非串内页旧槽(/f/xxx、时间线等):草稿功能已不覆盖这些页面,直接清理(含空值槽)
  // 仅在首次调用时执行一次(GM 标记);失败静默不阻塞。
  function migrateDraftSlotsToCanonical() {
    try {
      if (GM_getValue('xdex_draft_slots_migrated_v1', false)) return;
      const registry = getDraftRegistry();
      const keys = new Set(registry.slice());
      try {
        if (typeof GM_listValues === 'function') {
          GM_listValues().forEach((k) => { if (typeof k === 'string' && k.indexOf('xdex_draft:') === 0) keys.add(k); });
        }
      } catch (_) {}
      // 按规范槽分组(registry 顺序保序:越靠后越新)
      const groups = new Map(); // canonical -> [{storageKey, value}]
      keys.forEach((storageKey) => {
        const pathname = String(storageKey || '').slice('xdex_draft:'.length);
        if (!pathname) return;
        let value = '';
        try { value = GM_getValue(storageKey, ''); } catch (_) { value = ''; }
        if (typeof value !== 'string') value = '';
        const canonical = normalizeDraftKeyFromPath(pathname);
        const target = canonical || ''; // 非串内页 → 空组(统一清理)
        if (!groups.has(target)) groups.set(target, []);
        groups.get(target).push({ storageKey, value, pathname });
      });
      const registrySet = new Set(registry);
      let changed = false;
      groups.forEach((slots, canonical) => {
        if (!canonical) {
          // 非串内页槽:全部清理(空值或历史残留都清)
          slots.forEach((slot) => {
            try { deleteDraftSafe(slot.storageKey); } catch (_) {}
            registrySet.delete(slot.storageKey);
            changed = true;
          });
          return;
        }
        const canonicalStorageKey = getDraftStorageKey(canonical);
        let canonicalValue = '';
        try { canonicalValue = GM_getValue(canonicalStorageKey, ''); } catch (_) {}
        if (typeof canonicalValue !== 'string') canonicalValue = '';
        // 合并:规范槽优先;否则取 registry 顺序最靠后的非空槽(最近保存)
        let picked = canonicalValue ? null : null;
        let pickedFrom = canonicalStorageKey;
        if (!canonicalValue) {
          for (let i = slots.length - 1; i >= 0; i--) {
            if (slots[i].value) { picked = slots[i]; pickedFrom = slots[i].storageKey; break; }
          }
        }
        const finalValue = canonicalValue || (picked ? picked.value : '');
        if (finalValue && !canonicalValue) {
          try {
            GM_setValue(canonicalStorageKey, finalValue);
            registrySet.add(canonicalStorageKey);
            removeDraftDeletion(canonicalStorageKey);
            changed = true;
          } catch (_) {}
        }
        // 清理所有非规范槽
        slots.forEach((slot) => {
          if (slot.storageKey === canonicalStorageKey) return;
          try { deleteDraftSafe(slot.storageKey); } catch (_) {}
          registrySet.delete(slot.storageKey);
          changed = true;
        });
      });
      // 清理 registry 中已无值的键
      const nextRegistry = Array.from(registrySet).filter((k) => {
        try { const v = GM_getValue(k, ''); return typeof v === 'string' && v !== ''; } catch (_) { return false; }
      });
      if (changed || nextRegistry.length !== registry.length) saveDraftRegistry(nextRegistry);
      GM_setValue('xdex_draft_slots_migrated_v1', true);
      if (changed) console.info('[X岛-EX][草稿] 槽位规范化迁移完成');
    } catch (e) {
      console.warn('[X岛-EX][草稿] 槽位迁移失败', e);
    }
  }
  // 统一:安全删除草稿(有 GM_deleteValue 用之;否则写空串兜底)
  // ── 草稿删除账本: WebDAV 双向同步时传播"草稿被删除"状态 ──
  // 语义与分组设置删除账本一致: 删除总是赢, 重新编辑即翻案
  function getDraftDeletionStore() {
    try {
      const v = GM_getValue('xdex_draft_deletions', null);
      return (v && typeof v === 'object') ? v : {};
    } catch (_) { return {}; }
  }
  function canonicalDraftKey(anyKey) {
    const s = String(anyKey || '');
    if (!s) return '';
    return s.indexOf('xdex_draft:') === 0 ? s : getDraftStorageKey(s);
  }
  function registerDraftDeletion(storageKey) {
    const key = canonicalDraftKey(storageKey);
    if (!key) return;
    const store = getDraftDeletionStore();
    store[key] = Math.max(Number(store[key]) || 0, Date.now());
    try { GM_setValue('xdex_draft_deletions', store); } catch (_) {}
  }
  function removeDraftDeletion(storageKey) {
    const key = canonicalDraftKey(storageKey);
    if (!key) return;
    const store = getDraftDeletionStore();
    if (!(key in store)) return;
    delete store[key];
    try { GM_setValue('xdex_draft_deletions', store); } catch (_) {}
  }
  function deleteDraftSafe(key) {
    if (!key) key = getDraftKey();
    if (!key) return; // 非串内页(草稿键为空)无草稿可删,避免空键误删/空账本登记
    try {
      if (typeof GM_deleteValue === 'function') {
        GM_deleteValue(key);
      } else if (typeof GM_setValue === 'function') {
        GM_setValue(key, ''); // 覆盖为空字符串
      } else {
        // 无 GM_* 时不做处理
      }
    } catch (_) {}
    removeDraftKeyFromRegistry(key);
    registerDraftDeletion(key);   // 标记已删除, 供 WebDAV 同步压制远端旧草稿
  }
  function deleteAllDraftsSafe() {
    const keysToDelete = new Set(getDraftRegistry());
    try { keysToDelete.add(getDraftStorageKey()); } catch (_) {}
    try {
      if (typeof GM_listValues === 'function') {
        const allKeys = GM_listValues();
        allKeys.forEach((key) => {
          if (!isLegacyDraftKey(key)) return;
          let value = '';
          try { value = GM_getValue(key, ''); } catch (_) {}
          if (typeof value === 'string') keysToDelete.add(key);
        });
      }
    } catch (_) {}
    keysToDelete.forEach((key) => {
      try { deleteDraftSafe(key); } catch (_) {}
    });
    saveDraftRegistry([]);
  }
  // 完整移植为可调用函数。需要:jQuery 2.2.4+;GM_setValue/GM_getValue/GM_deleteValue 授权
  function enhanceIsland(config = {}) {
    return startupPerfDebug.measure('enhanceIsland', () => {
    // 配置开关(默认全开)
    let cfg = Object.assign({
      enablePreview: true,         // 发帖预览(插入预览DOM并实时渲染)
      enableDraft: true,           // 草稿保存/恢复和成功后清理
      enableAutoTitle: true,       // 自动设置网页标题(含页码)
      enableRelativeTime: true,    // 相对时间显示(每2.5秒刷新)
      enableQuoteInsert: true,     // 点击 No.xxxx 插入引用
      enablePasteImage: true       // 粘贴剪贴板图片到文件输入
    }, config);
    // 解析 jQuery
    const $ = cfg.$ || window.jQuery || window.$;
    if (!$) {
      console.warn('[enhanceIsland] jQuery not found.');
      return;
    }
    // 公用变量
    const 正文框 = $('textarea.h-post-form-textarea');
    const search = window.location.search;
    const 搜索参数 = {};
    search.replace(/^\?/, '').split('&').forEach(kev => {
      if (!kev) return;
      const [k, v] = kev.split('=', 2);
      搜索参数[k] = v;
    });
    const 路径 = window.location.pathname;
    const 路径分块 = 路径.split('/').splice(1);
    const isDraftEnabled = () => getDraftEnabledNow();
    let draftAutosaveBound = false;
    // 预览区域 DOM
    const previewHtml = buildEnhanceIslandPreviewHtml(true);
    //previewBox.outerHTML = previewHtml;
    // 引用插入函数(与原脚本一致)
    function enhanceNode(root) {
      if (typeof extendQuote === 'function') extendQuote(root);
      if (typeof initExtendedContent === 'function') initExtendedContent(root);
      if (typeof initContent === 'function') initContent(root);
      //if (typeof autoHideRefView === 'function') autoHideRefView(root);
    }
    // 只有在页面存在发帖表单容器时才插入预览
    function initPreviewBox() {
        if (!cfg.enablePreview) return;
        if (!$('#h-post-form form').length) return;
        // 只创建一次预览框;early skeleton 已存在时,替换为完整可绑定版本
        const $existingEarlyBox = $('.h-preview-box[data-xdex-early-preview="1"]').first();
        if (!$('.h-preview-box').length || $existingEarlyBox.length) {
            let $box;
            if ($existingEarlyBox.length) {
                $box = $(previewHtml).removeAttr('data-xdex-early-preview');
                $existingEarlyBox.replaceWith($box);
            } else {
                $box = $(previewHtml).removeAttr('data-xdex-early-preview').insertAfter('#h-post-form form');
            }
            setTimeout(() => {
              startupPerfDebug.measure('enhanceIsland.deferEnhanceNode', () => enhanceNode($box[0]), () => startupPerfDebug.summarizeRoot($box[0]));
            }, 0);
            // ★ 新增:让预览框里的图片也启用高清图+旋转布局逻辑
            if (typeof enableHDImage === 'function') {
                enableHDImage($box[0]);
            }
            // 让预览框宽度跟随表单
            // === 实时监测预览框父容器变化 ===
            const boxEl = $box[0];
            function applyBoxStyle(previewEl) {
              return startupPerfDebug.measure('enhanceIsland.applyBoxStyle', () => {
              if (!$box.closest('.qp-body').length) {
                // 不在 .qp-body 内时,应用基础样式
                $box.css({
                  width: '100%',
                  'box-sizing': 'border-box'
                });
                $box.find('.h-threads-content').css({
                  'overflow-wrap': 'break-word',
                  'word-break': 'break-word',
                  'white-space': 'normal'
                });
              } else {
                // 在 .qp-body 内时,实时跟随 .qp-content-wrap 宽度
                const wrapEl = $box.closest('.qp-content-wrap')[0];
                if (wrapEl && previewEl) {
                  const wrapStyle = window.getComputedStyle(wrapEl);
                  previewEl.style.width = wrapStyle.width;
                  previewEl.style.boxSizing = 'border-box';
                }
                // 保持换行规则
                $box.find('.h-threads-content').css({
                  'max-width': '100%',        /* 预览框不超过容器宽度 */
                  'overflow-wrap': 'break-word',
                  'word-break': 'break-word',
                  'white-space': 'normal'
                });
              }
              }, () => ({ previewBoxes: document.querySelectorAll('.h-preview-box').length }));
            }
            // 初始化时执行一次
            applyBoxStyle();
            // 监听 DOM 变化
            let previewStyleScheduled = false;
            const mo = new MutationObserver((mutations) => {
              startupPerfDebug.measureObserver('enhanceIsland.previewBoxStyle', mutations, () => {
              if (previewStyleScheduled) return;
              previewStyleScheduled = true;
              requestAnimationFrame(() => {
                previewStyleScheduled = false;
                applyBoxStyle();
              });
              }, () => ({
                previewBoxes: document.querySelectorAll('.h-preview-box').length
              }));
            });
            // 监听父节点变化(包括被移动到别的容器)
            mo.observe(document.body, {
              childList: true,
              subtree: true
            });
            if (typeof updatePreviewCookieId === 'function') updatePreviewCookieId();
            // === 图片预览更新函数 ===
            // 预览更新统一走全局 updatePreviewImageFromFile(实时查询当前活跃预览框),
            // 不再用闭包捕获的旧 $box,避免 REPLY 浮窗 open() 早于 enhanceIsland
            // 时粘贴图片更新到看不见的框。
            // === 监听文件选择 ===
            const fileInput = document.querySelector('input[type="file"][name="image"]');
            if (fileInput) {
                function syncPreviewFromCurrentFileInput() {
                    const file = fileInput.files && fileInput.files[0] ? fileInput.files[0] : null;
                    updatePreviewImageFromFile(file);
                }
                fileInput.addEventListener('change', syncPreviewFromCurrentFileInput);
                // 兼容粘贴事件先把图片写入 input.files、预览监听后绑定的竞态
                syncPreviewFromCurrentFileInput();
            }
            // 粘贴图片预览:使用全局幂等绑定,确保 REPLY 浮窗 early 打开时也能捕获
            bindPasteImagePreviewOnce();
        }
    }
    // 预览引用/隐藏文本渲染
    const previewBox = $('<div/>'); // 占位,真正引用在 initPreviewBox 后重新抓取
    const refExp = /^([>>]+.*)$/;
    const quoteTokenExp = /(?:>>)?No\.(\d{8})\b|(?:>>)?(?<!\d)(\d{8})(?!\d)/g;
    const hideExp = /\[h\]([\s\S]*?)\[\/h\]/g;
    let lastPreviewRaw = null;
    let lastPreviewRenderedHtml = '';
    let lastPreviewEnhanceAt = 0;
    let previewEnhanceTimer = null;
    function bindPreviewQuoteHover(previewRoot) {
      if (!previewRoot) return;
      if (typeof initContent === 'function') initContent(previewRoot);
    }
    function extendPreviewQuoteText(previewRoot) {
      if (!previewRoot) return;
      if (typeof extendQuote === 'function') extendQuote(previewRoot);
      bindPreviewQuoteHover(previewRoot);
    }
    function schedulePreviewEnhance(previewRoot) {
      if (!previewRoot) return;
      const now = Date.now();
      const elapsed = now - lastPreviewEnhanceAt;
      if (previewEnhanceTimer) {
        clearTimeout(previewEnhanceTimer);
        previewEnhanceTimer = null;
      }
      if (lastPreviewEnhanceAt === 0) {
        lastPreviewEnhanceAt = now;
        previewEnhanceTimer = setTimeout(() => {
          previewEnhanceTimer = null;
          lastPreviewEnhanceAt = Date.now();
          extendPreviewQuoteText(previewRoot);
        }, 0);
        return;
      }
      if (elapsed >= 3000) {
        lastPreviewEnhanceAt = now;
        extendPreviewQuoteText(previewRoot);
        return;
      }
      previewEnhanceTimer = setTimeout(() => {
        previewEnhanceTimer = null;
        lastPreviewEnhanceAt = Date.now();
        extendPreviewQuoteText(previewRoot);
      }, 3000 - elapsed);
    }
    function renderContent(raw) {
      return startupPerfDebug.measure('enhanceIsland.renderContent', () => {
      const box = $('.h-preview-box');
      if (!box.length) return;
      const previewContent = box.find('.h-threads-content');
      if (raw === lastPreviewRaw) return;
      lastPreviewRaw = raw;
      if (typeof raw !== 'string' || raw.trim() === '') {
        previewContent.text('');
        lastPreviewRenderedHtml = '';
        if (previewEnhanceTimer) {
          clearTimeout(previewEnhanceTimer);
          previewEnhanceTimer = null;
        }
        try {
          if (typeof window.__xdexScheduleReplyPanelFit === 'function') {
            window.__xdexScheduleReplyPanelFit();
          }
        } catch (_) {}
        return;
      }
      // 离屏拼好再一次写回,避免先清空再填充导致回复浮窗首字高度闪一下
      const $frag = $('<div></div>');
      previewContent[0]?.setAttribute('data-xdex-preview-spoiler-rendered', '1');
      function appendPreviewText(text, className) {
        if (text === '') return;
        const span = $('<span></span>').text(text);
        if (className) span.addClass(className);
        $frag.append(span);
      }
      function appendPreviewQuoteLine(line) {
        $frag.append($('<font color="#789922"></font>').text(line));
      }
      for (let i of raw.split('\n')) {
        i = i.replace(/ +/g, ' ');
        if (refExp.test(i)) {
          appendPreviewQuoteLine(i);
        } else {
          hideExp.lastIndex = 0;
          let lastIndex = 0;
          let match;
          let hasHiddenText = false;
          while ((match = hideExp.exec(i)) !== null) {
            hasHiddenText = true;
            appendPreviewText(i.slice(lastIndex, match.index));
            appendPreviewText(match[1], 'h-hidden-text');
            lastIndex = hideExp.lastIndex;
          }
          if (hasHiddenText) {
            appendPreviewText(i.slice(lastIndex));
          } else {
            appendPreviewText(i);
          }
        }
        $frag.append('<br>');
      }
      const renderedHtml = $frag[0]?.innerHTML || '';
      const previewChanged = renderedHtml !== lastPreviewRenderedHtml;
      lastPreviewRenderedHtml = renderedHtml;
      if (previewChanged) {
        // 旧:previewContent.text(''); 再逐段 append
        // 一次替换,减少中间空高被 auto-fit 量到
        previewContent.empty().append($frag.contents());
        bindPreviewQuoteHover(previewContent[0]);
        schedulePreviewEnhance(previewContent[0]);
        // 预览空→有 后立刻排队贴高,避免 RO 晚到期间 overflow:auto 闪滚动条
        try {
          if (typeof window.__xdexScheduleReplyPanelFit === 'function') {
            window.__xdexScheduleReplyPanelFit();
          }
        } catch (_) {}
      }
      // 自动识别链接
      if (typeof runAutoUrlLinkify === 'function') {
        runAutoUrlLinkify(previewContent[0]);
      }
      // 标记饼干背景色
      if (typeof markAllCookies === 'function') {
        try {
          const cfg = getFilterConfig();
          markAllCookies(cfg.markedGroups || [], previewContent[0]);
        } catch (e) {}
      }
      }, () => ({
        textLength: typeof raw === 'string' ? raw.length : 0,
        lines: typeof raw === 'string' && raw ? raw.split('\n').length : 0,
        previewBoxes: document.querySelectorAll('.h-preview-box').length
      }));
    }
  // 草稿:发帖成功清空(拦截模式优先)
  function 清空编辑(key) {
    if (!isDraftEnabled()) return;
    if (!key) key = getDraftKey();
    if (!key) return; // 非串内页无草稿可清
    if (key) {
      deleteDraftSafe(getDraftStorageKey(key));
      deleteDraftSafe(key);
      return;
    }
    // ===== 兼容原“中间页”的旧逻辑(仍保留)=====
    const okBox = document.getElementsByClassName('success')[0];
    if (!okBox) return;
    if (!okBox.textContent.includes('回复成功')) return;
    const hrefEl = document.getElementById('href');
    if (!hrefEl || !hrefEl.href) return;
    const m = /https?:\/\/[^/]+(\/t\/\d+)/.exec(hrefEl.href);
    if (!m) return;
    deleteDraftSafe(getDraftStorageKey(m[1]));
    deleteDraftSafe(m[1]);
  }
    // 统一用事件清空,缺省用 getDraftKey()
    document.addEventListener('replySuccess', e => {
      清空编辑(e.detail?.key || getDraftKey());
    });
    // 草稿:载入
    function 载入编辑() {
      if (!isDraftEnabled()) return;
      if (!正文框.length) return;
      const key = getDraftKey();
      if (!key) return; // 非串内页(板块页等)无草稿
      const migratedLegacy = migrateLegacyDraftIfNeeded(key);
      let draft = readDraftValue(key);
      if (!draft && migratedLegacy) draft = migratedLegacy;
      if (typeof draft !== 'string') draft = '';
      const ta = 正文框[0];
      const current = ta ? String(ta.value || '') : '';
      const rId = 搜索参数 && 搜索参数.r ? String(搜索参数.r) : '';
      const autoQuote = rId ? `>>No.${rId}\n` : '';
      const currentTrim = current.trim();
      // 仅当文本框为空,或仍是站点/脚本自动插入的单独引用行时,才允许草稿覆盖
      // 避免页面未完全加载时用户已输入内容被 载入编辑 整框冲掉
      const currentIsOnlyAutoQuote = !!(rId && (
        current === autoQuote
        || currentTrim === `>>No.${rId}`
      ));
      if (currentTrim !== '' && !currentIsOnlyAutoQuote) {
        return;
      }
      // 关闭引用开启时:不在草稿载入阶段强制前置 r= 自动引用(与 tag27 一致)
      // 值班室/举报表单仍保留自动引用,方便精准举报
      let shouldAutoPrependQuote = !!rId;
      if (shouldAutoPrependQuote) {
        try {
          const s = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
          const disableAutoQuote = s.disableAutoQuote !== false;
          if (disableAutoQuote) {
            let preserveForReport = false;
            try {
              const decodedPath = decodeURIComponent(location.pathname || '');
              if (/^\/f\/值班室(?:\/|$)/.test(decodedPath)) preserveForReport = true;
            } catch (e) {}
            if (!preserveForReport) {
              try {
                preserveForReport = !!document.querySelector('form[action="/Home/Forum/doReplyThread.html"] #h-report-switch, form[action="/Home/Forum/doPostThread.html"] #h-report-switch, #h-report-switch');
              } catch (e) {}
            }
            if (!preserveForReport) shouldAutoPrependQuote = false;
          }
        } catch (e) {}
      }
      if (shouldAutoPrependQuote) {
        const quote = `>>No.${rId}\n`;
        if (!String(draft).startsWith(quote) && !String(draft).startsWith(`>>No.${rId}`)) {
          draft = quote + draft;
        }
      }
      // 内容没变化则不写回,减少无意义 input/草稿回写
      if (current === draft) return;
      正文框.val(draft);
      // 触发 input 事件,让字数计数器同步更新(jQuery .val() 不会触发原生事件)
      正文框[0]?.dispatchEvent(new Event('input', { bubbles: true }));
    }
    // 草稿:注册自动保存 + 初始化一次保存触发(原脚本用 $(保存编辑))
function 注册自动保存编辑() {
      if (draftAutosaveBound) {
        保存编辑();
        return;
      }
      draftAutosaveBound = true;
      // 原有正文框监听
      $('form').on('input', 保存编辑);
      // 颜文字选择变更时也更新预览(原生代码修改textarea值但不派发input事件)
      $('form').on('change', '#h-emot-select', 保存编辑);
      // 改为事件委托:监听 name 和 title 输入框
      $(document).on('input', 'form input[name="name"], form input[name="title"]', 保存编辑);
      // 文档就绪时同步一次
      $(保存编辑);
    }
    function 保存编辑() {
      return startupPerfDebug.measure('enhanceIsland.saveDraftAndPreview', () => {
      if (!正文框.length) return;
      if (isDraftEnabled()) {
        const draftKey = getDraftKey();
        if (draftKey) saveDraftValue(draftKey, 正文框.val());
      }
      renderContent(正文框.val());
      const box = $('.h-preview-box');
      if (box.length) {
        const previewTitle = box.find('.h-threads-info-title');
        const previewEmail = box.find('.h-threads-info-email');
        const titleVal = $('form input[name="title"]').val() || '';
        const nameVal  = $('form input[name="name"]').val() || '';
        // 标题:空则隐藏,有值则显示并更新
        if (titleVal.trim() === '') {
          previewTitle.hide().text('无标题'); // 保留默认文案但不显示
        } else {
          previewTitle.text(titleVal.trim()).show();
        }
        // 名称:空则隐藏,有值则显示并更新
        if (nameVal.trim() === '') {
          previewEmail.hide().text('无名氏'); // 保留默认文案但不显示
        } else {
          previewEmail.text(nameVal.trim()).show();
        }
      }
      }, () => ({ textLength: 正文框 && 正文框.val ? String(正文框.val() || '').length : 0 }));
    }
    // 点击 No.xxxx 插入引用(保持原先光标与选择区逻辑)
    function 注册追记引用串号() {
      // 预览占位编号点击会夺走文本框焦点(浏览器在 mousedown 阶段转移焦点,click 拦不住),
      // 这里在 mousedown 时记录光标,click 确认是预览编号后把焦点和光标恢复到原位置
      $('body').on('mousedown', 'a.h-threads-info-id', () => {
        const el = 正文框[0];
        if (!el) return;
        window.__xdexPreviewFocusRestore = {
          el,
          start: el.selectionStart,
          end: el.selectionEnd,
          hadFocus: document.activeElement === el,
        };
      });
      $('body').on('click', 'a.h-threads-info-id', e => {
        // 如果按住 Ctrl/Meta/Shift 键,允许浏览器默认行为(在新标签页/新窗口打开链接)
        if (e.ctrlKey || e.metaKey || e.shiftKey) return;
        e.preventDefault();
        // 发言预览内的占位编号不参与引用追记(保留 href="javascript:;" 以兼容站点 attr(href).replace,
        // 排除只按 .h-preview-box 祖先判断,不依赖 href 推断)
        if (e.currentTarget.closest('.h-preview-box')) {
          e.stopPropagation();
          // 点击预览占位编号:把焦点和光标恢复到文本框原位置
          const r = window.__xdexPreviewFocusRestore;
          delete window.__xdexPreviewFocusRestore;
          if (r && r.el && r.el.isConnected) {
            r.el.focus({ preventScroll: true });
            r.el.setSelectionRange(r.start, r.end);
          }
          return;
        }
        if (!正文框.length) return;
        // 失焦时 selectionEnd 可能跳到文本末尾,先聚焦再读取准确选区
        const el = 正文框[0];
        const wasActive = document.activeElement;
        if (el && document.activeElement !== el) el.focus();
        const start = 正文框.prop('selectionStart');
        const end = 正文框.prop('selectionEnd');
        const str = 正文框.val();
        const left = str.substring(0, start);
        const right = str.substring(end);
        const ref = `>>${e.currentTarget.textContent.trim()}`;
        console.log('[引用追记]', {
          wasActive: wasActive?.tagName,
          didFocus: document.activeElement !== wasActive,
          start, end, strLen: str.length,
          hasSelection: start !== end,
          leftLen: left.length, rightLen: right.length, ref
        });
        if (start !== end) {
          // 有选区 → 替换选区内容
          console.log('[引用追记] 走替换选区分支, left长度:', left.length, 'right长度:', right.length);
          正文框.val(`${left}${ref}${right}`);
        } else {
          // 无选区 → 根据光标位置插入
          const branch = start === 0 ? '开头' : start === str.length ? '末尾' : '中间';
          console.log('[引用追记] 走无选区分支:', branch, 'start:', start);
          const _newVal = (() => {
            if (start === 0)          return `${ref}\n${right}`;
            if (start === str.length) return `${left}\n${ref}\n`;
            return `${left}\n${ref}\n${right}`;
          })();
          正文框.val(_newVal);
        }
        // 旧:正文框.trigger('input', '');
        // jQuery .trigger('input') 不会触发原生 addEventListener('input'),
        // 导致 tag6 字数统计要等用户继续输入才刷新
        正文框.trigger('input', '');
        if (el) el.dispatchEvent(new Event('input', { bubbles: true }));
        保存编辑();
        // 光标定位到引用号之后——必须延迟到 click 事件彻底结束,
        // 否则浏览器会把焦点还给被点击的 <a> 元素
        // 旧:const _cursorPos = left.length + ref.length + (start !== end ? 0 : 1);
        // 无选区时插入形态为(开头)`ref\n` 或(非开头)`\nref\n`,光标需落在引用号后的那个换行之后
        const _cursorPos = start !== end
          ? left.length + ref.length
          : left.length + (start === 0 ? 0 : 1) + ref.length + 1;
        // 保存期望光标位置,供浮窗重新打开时恢复
        el.__pendingQuoteCursorPos = _cursorPos;
        setTimeout(() => {
          el.focus();
          el.setSelectionRange(_cursorPos, _cursorPos);
          // 若 focus 成功,清除 pending 标记
          if (document.activeElement === el) {
            el.__pendingQuoteCursorPos = null;
          }
        }, 0);
      });
    }
    // 粘贴图片到文件输入(保持原选择器)
    function 注册粘贴图片() {
      if (!cfg.enablePasteImage) return;
      window.addEventListener('paste', e => {
        const files = (e.clipboardData || e.originalEvent?.clipboardData)?.files || [];
        if (files.length) {
          const fileInput = document.querySelector('input[type="file"][name="image"]');
          if (fileInput) {
            fileInput.files = files;
            fileInput.dispatchEvent(new Event('input', { bubbles: true }));
            fileInput.dispatchEvent(new Event('change', { bubbles: true }));
          }
        }
      });
    }
    // 子函数:选择了图片后出现“清除图片”按钮;清除后按钮消失,恢复“选择文件”
    function 绑定清除图片按钮() {
        const $form = $('#h-post-form form, form[action="/Home/Forum/doReplyThread.html"]').first();
        if (!$form.length) return;
        const $file = $form.find('input[type="file"][name="image"]');
        if (!$file.length) return;
        if ($file.data('xdexClearBound')) return;
        $file.data('xdexClearBound', true);
        // 包一层容器,方便布局
        if (!$file.parent().hasClass('xdex-file-wrapper')) {
            $file.wrap('<div class="xdex-file-wrapper" style="display:flex;align-items:center;justify-content:space-between;width:100%;"></div>');
        }
      function 刷新按钮() {
          const hasFile = $file[0].files && $file[0].files.length > 0;
          let $btn = $form.find('.xdex-clear-image-btn');
          if (hasFile) {
              if (!$btn.length) {
                  $btn = $('<button type="button" class="xdex-clear-image-btn xdex-icon-btn" title="清除图片">').html(XDEX_SVG_X_ICON);
                  $file.after($btn);
                  $btn.on('click', function (e) {
                    e.stopPropagation();
                    e.preventDefault();
                    // 清空文件
                    $file.val('');
                    // 直接调用预览清空逻辑(全局函数;此前误用 initPreviewBox 内的
                    // updatePreviewFromFile,作用域不可达导致恒走兜底分支,
                    // 去重键因此从未重置,同一个 HEIC 重新附带时不再提示)
                    if (typeof updatePreviewImageFromFile === 'function') {
                        updatePreviewImageFromFile(null);
                    } else {
                        const $preview = $('.h-preview-box');
                        $preview.find('img').attr('src', '').removeAttr('src');
                        $preview.find('.h-threads-img-a').attr('href', '');
                    }
                    // 复原预览框容器状态
                    const $previewBox = $('.h-preview-box .h-threads-img-box');
                    $previewBox.removeClass('h-active');
                    $previewBox.find('.h-threads-img-a').css('height', '');
                    $previewBox.find('.h-threads-img')
                      .css({ transform: '', top: '0px', left: '0px' })
                      .removeAttr('data-rotate-index');
                    // 移除按钮
                    $(this).remove();
                });
              }
          } else {
              if ($btn.length) $btn.remove();
              // 原生 input 被清空(选择框取消/浏览器行为)时同样要复位 HEIC 去重,
              // 否则下一次重新附带同一文件不会再提示
              if (typeof resetHeifPreviewTipState === 'function') resetHeifPreviewTipState();
          }
      }
      $file.on('change', 刷新按钮);
      刷新按钮();
    }
    // 自动标题:择标题(与原逻辑等价)
    function 选择标题() {
      return selectEnhanceIslandTitleText() || document.title;
    }
    const 原始标题 = getEnhanceIslandOriginalTitle();
    function 自动标题() {
      if (!cfg.enableAutoTitle) return;
      // 每次调用时重新解析 URL
      const search = window.location.search;
      const 搜索参数 = {};
      search.replace(/^\?/, '').split('&').forEach(kev => {
        if (!kev) return;
        const [k, v] = kev.split('=', 2);
        搜索参数[k] = v;
      });
      const 路径 = window.location.pathname;
      const 路径分块 = 路径.split('/').splice(1);
      const 是串页 = 路径分块[0] === 't' || (路径分块[0] === 'Forum' && 路径分块[1] === 'po' && 路径分块[2] === 'id');
      if (!是串页) return;
      const 页码 = 路径分块[0] === 'Forum'
        ? (路径分块[5]?.replace(/\.html$/, '') || 1)
        : (搜索参数.page || 1);
      const 标题 = 选择标题();
      const titleEl = document.querySelector('title');
      if (titleEl) {
        titleEl.textContent = `${标题} - ${原始标题} - page ${页码}`;
      }
    }
    // 相对时间格式化(与原逻辑等价,目标 span.h-threads-info-createdat)
    function getFriendlyTime(machineReadableTime) {
      const date = new Date(machineReadableTime);
      const now = new Date();
      if (now < date) return machineReadableTime;
      let friendlyDate = machineReadableTime.slice(0, 10);
      let friendlyTime = machineReadableTime.slice(13, 21);
      const weekday = machineReadableTime.slice(11, 12);
      const diff = (now.getTime() - date.getTime()) / 1000;
      if (diff < 60) {
        friendlyTime = `${Math.floor(diff)}秒前`;
      } else if (diff < 3600) {
        friendlyTime = `${Math.floor(diff / 60)}分钟前`;
      } else if (diff < 24 * 3600) {
        friendlyTime = `${Math.floor(diff / 3600)}小时前 ${friendlyTime}`;
      }
      const yesterday = new Date(new Date(now - 1000 * 60 * 60 * 24).toLocaleDateString());
      if (now.toLocaleDateString() === date.toLocaleDateString()) {
        friendlyDate = '今天';
      } else if (yesterday.toLocaleDateString() === date.toLocaleDateString()) {
        friendlyDate = '昨天';
      } else if (yesterday - date < 1000 * 60 * 60 * 24 * 30) {
        friendlyDate = `${Math.floor((now - date) / (1000 * 60 * 60 * 24))}天前`;
      } else if (now.getFullYear() === date.getFullYear()) {
        friendlyDate = friendlyDate.slice(5);
      } else {
        friendlyDate = `${now.getFullYear() - date.getFullYear()}年前 ${friendlyDate}`;
      }
      return `${friendlyDate}(${weekday})${friendlyTime}`;
    }
    function formatDateStrOnPage(root = document) {
      if (!cfg.enableRelativeTime) return;
      if (document.visibilityState && document.visibilityState !== 'visible') return;
      if (getTimeDisplayMode() === 'exact') {
        restoreExactDateStrOnPage(root);
        return;
      }
      const $root = root && root.jquery ? root : $(root || document);
      const targets = withSelf($root, 'span.h-threads-info-createdat');
      targets.each(function () {
        const target = $(this);
        const timeStr = target.attr('data-xdex-original-time') || target.attr('title') || target.text().trim();
        if (!timeStr) return;
        const date = new Date(timeStr);
        if (Number.isNaN(date.getTime())) return;
        // 写前先比较:相对时间实际一分钟才变一次,无条件写 attr/text 会让每 5s 的全量遍历把整页版面弄脏,
        // 紧接着的 rect 读取就退化成强制同步重排(表现为每 5s 一次可感知微顿)
        if (target.attr('data-xdex-original-time') !== timeStr) target.attr('data-xdex-original-time', timeStr);
        if (target.attr('title') !== timeStr) target.attr('title', timeStr);
        const friendlyTime = getFriendlyTime(timeStr);
        if (target.text() !== friendlyTime) target.text(friendlyTime);
      });
    }
    function getTimeDisplayMode() {
      try {
        const saved = GM_getValue(SettingPanel.key, {});
        return saved && saved.timeDisplayMode === 'exact' ? 'exact' : 'relative';
      } catch (e) {
        return cfg.timeDisplayMode === 'exact' ? 'exact' : 'relative';
      }
    }
    function restoreExactDateStrOnPage(root = document) {
      if (document.visibilityState && document.visibilityState !== 'visible') return;
      const $root = root && root.jquery ? root : $(root || document);
      const targets = withSelf($root, 'span.h-threads-info-createdat');
      targets.each(function () {
        const target = $(this);
        const timeStr = target.attr('data-xdex-original-time') || target.attr('title');
        if (timeStr && target.text() !== timeStr) target.text(timeStr); // 同样写前比较,避免无变化的整页脏化
      });
    }
    function applyTimeDisplayMode(root = document) {
      if (!cfg.enableRelativeTime) return;
      if (getTimeDisplayMode() === 'exact') {
        restoreExactDateStrOnPage(root);
        return;
      }
      formatDateStrOnPage(root);
    }
    function stopRelativeTimeScheduler() {
      if (window.__xdexRelativeTimeTimer) {
        clearInterval(window.__xdexRelativeTimeTimer);
        window.__xdexRelativeTimeTimer = null;
      }
      if (window.__xdexRelativeTimeVisibilityHandler) {
        document.removeEventListener('visibilitychange', window.__xdexRelativeTimeVisibilityHandler);
        window.__xdexRelativeTimeVisibilityHandler = null;
      }
    }
    function ensureRelativeTimeScheduler() {
      if (!cfg.enableRelativeTime) {
        stopRelativeTimeScheduler();
        return;
      }
      if (!window.__xdexRelativeTimeVisibilityHandler) {
        window.__xdexRelativeTimeVisibilityHandler = () => {
          if (document.visibilityState === 'visible') {
            applyTimeDisplayMode();
          }
        };
        document.addEventListener('visibilitychange', window.__xdexRelativeTimeVisibilityHandler, { passive: true });
      }
      if (!window.__xdexRelativeTimeTimer) {
        window.__xdexRelativeTimeTimer = setInterval(() => {
          applyTimeDisplayMode();
        }, 5000);
      }
      if (!document.visibilityState || document.visibilityState === 'visible') {
        applyTimeDisplayMode();
      }
    }
    window.__xdexApplyTimeDisplayMode = applyTimeDisplayMode;
    // 路由:各页面初始化(与原逻辑一致)
    function 串() {
      if (cfg.enablePreview) initPreviewBox();
      if (isDraftEnabled()) 载入编辑();
      if (cfg.enableQuoteInsert) 注册追记引用串号();
      if (cfg.enablePreview || isDraftEnabled()) 注册自动保存编辑();
      if (cfg.enablePasteImage) 注册粘贴图片();
      绑定清除图片按钮();
      if (cfg.enableAutoTitle) 自动标题();
      ensureRelativeTimeScheduler();
    }
    function 串只看po() {
      if (cfg.enableAutoTitle) 自动标题();
      ensureRelativeTimeScheduler();
    }
    function 版块() {
      if (cfg.enablePreview) initPreviewBox();
      if (cfg.enablePreview || isDraftEnabled()) 注册自动保存编辑();
      //if (cfg.enableQuoteInsert) 注册追记引用串号();
      if (cfg.enablePasteImage) 注册粘贴图片();
      绑定清除图片按钮();
      ensureRelativeTimeScheduler();
    }
    function 时间线() {
      if (cfg.enablePreview) initPreviewBox();
      if (cfg.enablePreview || isDraftEnabled()) 注册自动保存编辑();
      //if (cfg.enableQuoteInsert) 注册追记引用串号();
      if (cfg.enablePasteImage) 注册粘贴图片();
      绑定清除图片按钮();
      ensureRelativeTimeScheduler();
    }
    function 回复成功() {
      if (isDraftEnabled()) 清空编辑();
      if (cfg.enablePasteImage) 注册粘贴图片();
    }
    document.addEventListener('replySuccess', e => {
        回复成功(e.detail?.key);
    });
    function 未知() {
      if (cfg.enablePasteImage) 注册粘贴图片();
    }
    // ★ 草稿槽规范化迁移(串级共享单槽;一次性,GM 标记防重)
    try { migrateDraftSlotsToCanonical(); } catch (e) {}
    // 一级路径解析(支持 m 前缀)
    const 一层路径 = 路径分块[0] === 'm' ? 路径分块[1] : 路径分块[0];
    // 入口分流(与原脚本一致)
    switch (一层路径) {
      case 't':
        串();
        break;
      case 'f':
        版块();
        break;
      case 'Forum':
        if (路径分块[1] === 'po' && 路径分块[2] === 'id') {
          串只看po();
        } else if (路径分块[1] === 'timeline' && 路径分块[2] === 'id') {
          // 这里就是时间线页面
          时间线();
        } else {
          未知();
        }
        break;
      case 'Home':
        if (路径 === '/Home/Forum/doReplyThread.html') {
          回复成功();
        } else {
          未知();
        }
        break;
      case 'Member':
        if (路径.startsWith('/Member/User/Cookie/export/id/')) {
          console.debug('//不是我的TODO');
        }
        break;
      default:
        未知();
    }
    // 首次渲染预览(若需要)
    if (cfg.enablePreview) {
      renderContent(正文框.val ? (正文框.val() || '') : '');
    }
    // 以便无缝翻页后修改标题
    window.enhanceIslandAutoTitle = 自动标题;
    }, () => {
      const textarea = document.querySelector('textarea.h-post-form-textarea');
      return { path: location.pathname, textLength: textarea ? String(textarea.value || '').length : 0 };
    });
  }

  /* --------------------------------------------------
   * tag 15. 板块页快速回复入口(含时间线支持)
   * -------------------------------------------------- */
  function replyQuicklyOnBoardPage() {
    // 同时识别 /f/ 板块 和 /Forum/timeline/id/{id} 时间线
    const isBoardPage = PageType.isBoardPage();
    const timelineInfo = PageType.getTimelineInfo();
    const isTimeline = !!timelineInfo;
    if (!isBoardPage && !isTimeline) return;
    // 时间线 id 与名称映射(1-7)
    const timelineId = timelineInfo ? timelineInfo.id : null;
    const timelineNameMap = {
      '1': '综合线',
      '2': '创作线',
      '3': '非创作线',
      '4': '亚文化线',
      '5': '综合2线',
      '6': '游戏线',
      '7': '生活线'
    };
    // boardBaseName:仅板块/时间线的基础名称(不包含后缀),显示文本按模式拼接后缀
    const boardBaseName = isTimeline
      ? (timelineNameMap[timelineId] || '时间线')
      : decodeURIComponent((location.pathname || '').replace(/^\/f\//, '').split('/')[0] || '');
    // 持久变量:保存当前正在回复的串号 / 缓存的回复参数
    let currentReplyTid = null;
    let pendingReplyParams = null; // { tid, resto, hash }
    // ---------- 子函数:为时间线插入回复表单(第四个子函数) ----------
    function bindTimelineReplyForm() {
      // 找到页面上的第一个 <hr>,在其下插入我们要的回复表单容器与一个 <hr> 与线程列表分隔
      const $firstHr = $('hr').first();
      if (!$firstHr.length) return;
      const timelineLabel = timelineNameMap[timelineId] || '时间线';
      const emotOptions = [
        '<option value="|∀゚">|∀゚</option>',
        '<option value="(´゚Д゚`)">(´゚Д゚`)</option>',
        '<option value="(;´Д`)">(;´Д`)</option>',
        '<option value="(`・ω・)">(`・ω・)</option>',
        '<option value="(=゚ω゚)=">(=゚ω゚)=</option>',
        '<option value="| ω・´)">| ω・´)</option>',
        '<option value="|-` )">|-` )</option>',
        '<option value="|д` )">|д` )</option>',
        '<option value="|ー` )">|ー` )</option>',
        '<option value="|∀` )">|∀` )</option>',
        '<option value="(つд⊂)">(つд⊂)</option>',
        '<option value="(゚Д゚≡゚Д゚)">(゚Д゚≡゚Д゚)</option>',
        '<option value="(^o^)ノ">(^o^)ノ</option>',
        '<option value="(|||゚Д゚)">(|||゚Д゚)</option>',
        '<option value="( ゚∀゚)">( ゚∀゚)</option>',
        '<option value="( ´∀`)">( ´∀`)</option>',
        '<option value="(*´∀`)">(*´∀`)</option>',
        '<option value="(*゚∇゚)">(*゚∇゚)</option>',
        '<option value="(*゚ー゚)">(*゚ー゚)</option>',
        '<option value="( ゚ 3゚)">( ゚ 3゚)</option>',
        '<option value="( ´ー`)">( ´ー`)</option>',
        '<option value="( ・_ゝ・)">( ・_ゝ・)</option>',
        '<option value="( ´_ゝ`)">( ´_ゝ`)</option>',
        '<option value="(*´д`)">(*´д`)</option>',
        '<option value="(・ー・)">(・ー・)</option>',
        '<option value="(・∀・)">(・∀・)</option>',
        '<option value="(ゝ∀・)">(ゝ∀・)</option>',
        '<option value="(〃∀〃)">(〃∀〃)</option>',
        '<option value="(*゚∀゚*)">(*゚∀゚*)</option>',
        '<option value="( ゚∀。)">( ゚∀。)</option>',
        '<option value="( `д´)">( `д´)</option>',
        '<option value="(`ε´ )">(`ε´ )</option>',
        '<option value="(`ヮ´ )">(`ヮ´ )</option>',
        '<option value="σ`∀´)">σ`∀´)</option>',
        '<option value=" ゚∀゚)σ"> ゚∀゚)σ</option>',
        '<option value="゚ ∀゚)ノ">゚ ∀゚)ノ</option>',
        '<option value="(╬゚д゚)">(╬゚д゚)</option>',
        '<option value="(|||゚д゚)">(|||゚д゚)</option>',
        '<option value="( ゚д゚)">( ゚д゚)</option>',
        '<option value="Σ( ゚д゚)">Σ( ゚д゚)</option>',
        '<option value="( ;゚д゚)">( ;゚д゚)</option>',
        '<option value="( ;´д`)">( ;´д`)</option>',
        '<option value="( д ) ゚ ゚">( д ) ゚ ゚</option>',
        '<option value="( ☉д⊙)">( ☉д⊙)</option>',
        '<option value="((( ゚д゚)))">((( ゚д゚)))</option>',
        '<option value="( ` ・´)">( ` ・´)</option>',
        '<option value="( ´д`)">( ´д`)</option>',
        '<option value="( -д-)">( -д-)</option>',
        '<option value="(&gt;д&lt;)">(&gt;д&lt;)</option>',
        '<option value="・゚( ノд`゚)">・゚( ノд`゚)</option>',
        '<option value="( TдT)">( TдT)</option>',
        '<option value="( ̄∇ ̄)">( ̄∇ ̄)</option>',
        '<option value="( ̄3 ̄)">( ̄3 ̄)</option>',
        '<option value="( ̄ー ̄)">( ̄ー ̄)</option>',
        '<option value="( ̄ .  ̄)">( ̄ .  ̄)</option>',
        '<option value="( ̄皿 ̄)">( ̄皿 ̄)</option>',
        '<option value="( ̄艸 ̄)">( ̄艸 ̄)</option>',
        '<option value="( ̄︿ ̄)">( ̄︿ ̄)</option>',
        '<option value="( ̄︶ ̄)">( ̄︶ ̄)</option>',
        '<option value="ヾ(´ω゚`)">ヾ(´ω゚`)</option>',
        '<option value="(*´ω`*)">(*´ω`*)</option>',
        '<option value="(・ω・)">(・ω・)</option>',
        '<option value="( ´・ω)">( ´・ω)</option>',
        '<option value="(`・ω)">(`・ω)</option>',
        '<option value="(´・ω・`)">(´・ω・`)</option>',
        '<option value="(`・ω・´)">(`・ω・´)</option>',
        '<option value="( `_っ´)">( `_っ´)</option>',
        '<option value="( `ー´)">( `ー´)</option>',
        '<option value="( ´_っ`)">( ´_っ`)</option>',
        '<option value="( ´ρ`)">( ´ρ`)</option>',
        '<option value="( ゚ω゚)">( ゚ω゚)</option>',
        '<option value="(o゚ω゚o)">(o゚ω゚o)</option>',
        '<option value="( ^ω^)">( ^ω^)</option>',
        '<option value="(。◕∀◕。)">(。◕∀◕。)</option>',
        '<option value="/( ◕‿‿◕ )\\">/( ◕‿‿◕ )\\</option>',
        '<option value="ヾ(´ε`ヾ)">ヾ(´ε`ヾ)</option>',
        '<option value="(ノ゚∀゚)ノ">(ノ゚∀゚)ノ</option>',
        '<option value="(σ゚д゚)σ">(σ゚д゚)σ</option>',
        '<option value="(σ゚∀゚)σ">(σ゚∀゚)σ</option>',
        '<option value="|д゚ )">|д゚ )</option>',
        '<option value="┃電柱┃">┃電柱┃</option>',
        '<option value="゚(つд`゚)">゚(つд`゚)</option>',
        '<option value="゚Å゚ ) ">゚Å゚ ) </option>',
        '<option value="⊂彡☆))д`)">⊂彡☆))д`)</option>',
        '<option value="⊂彡☆))д´)">⊂彡☆))д´)</option>',
        '<option value="⊂彡☆))∀`)">⊂彡☆))∀`)</option>',
        '<option value="(´∀((☆ミつ">(´∀((☆ミつ</option>',
        '<option value="・゚( ノヮ´ )">・゚( ノヮ´ )</option>',
        '<option value="(ノ)`ω´(ヾ)">(ノ)`ω´(ヾ)</option>',
        '<option value="ᕕ( ᐛ )ᕗ">ᕕ( ᐛ )ᕗ</option>',
        '<option value="( ˇωˇ)">( ˇωˇ)</option>',
        '<option value="( 」゚Д゚)」&lt;">( 」゚Д゚)」&lt;</option>',
        '<option value="( ›´ω`‹ )">( ›´ω`‹ )</option>',
        '<option value="(;´ヮ`)7">(;´ヮ`)7</option>',
        '<option value="(`ゥ´ )">(`ゥ´ )</option>',
        '<option value="(`ᝫ´ )">(`ᝫ´ )</option>',
        '<option value="( ᑭ`д´)ᓀ))д´)ᑫ">( ᑭ`д´)ᓀ))д´)ᑫ</option>',
        '<option value="σ( ᑒ )">σ( ᑒ )</option>',
        '<option value="(`ヮ´ )σ`∀´) ゚∀゚)σ">齐齐蛤尔</option>',
        '<option value="吁~~~~  rnm,退钱!<br>&nbsp;   /   / <br>( ゚ 3゚) `ー´) `д´) `д´)">大嘘</option>',
        '<option value="[h] [/h]">防剧透</option>',
        '<option value="[n]">骰子</option>',
        '<option value="[n,m]">高级骰子</option>'
      ].join('\n');
      const formHtml = `
        <div id="h-post-form" class="uk-container-center uk-width-small-8-10 uk-width-medium-4-10 uk-width-large-4-10">
          <form action="/Home/Forum/doReplyThread.html" method="post" enctype="multipart/form-data">
            <!-- 隐藏字段,默认用 与 原来 setMode 一致的占位值(会在点击链接时被替换) -->
            <input type="hidden" name="resto" value="20011114">
            <input type="hidden" name="__hash__" value="cirns">
            <!-- 回应模式行(包含兼容原来逻辑的类和占位)-->
            <div class="uk-grid uk-grid-small h-post-form-grid js-reply-mode-row">
              <div class="uk-width-1-5">
                <div class="h-post-form-title">回应模式</div>
              </div>
              <div class="h-post-form-input uk-width-4-5" style="display:flex;align-items:center;justify-content:space-between;">
                <span class="js-reply-mode-text">${timelineLabel}-快速回复</span>
                <span class="xdex-cookie-check-area" style="display:flex;align-items:center;gap:4px;flex-shrink:0;"></span>
                <div class="reply-mode-toggle" style="display:flex;flex-direction:row;align-items:center;gap:6px;">
                  <span class="js-reply-extra" style="display:none;display:inline-flex;align-items:center;"></span>
                  <button type="button" class="js-toggle-mode xdex-reply-mode-btn" title="当前为&quot;回复&quot;模式,点击切换为&quot;发串&quot;模式">${XDEX_SVG_SWITCH}</button>
                </div>
              </div>
            </div>
            <!-- 以下为你提供的表单其余内容(保留原样,确保 textarea, input 名称等一致) -->
            <div class="uk-grid uk-grid-small h-post-form-grid">
              <div class="uk-width-1-5">
                <div class="h-post-form-title">名 称</div>
              </div>
              <div class="h-post-form-input uk-width-3-5">
                <input type="text" name="name" size="28" value="" maxlength="100">
              </div>
              <div class="h-post-form-option uk-width-1-5">
                <label class="h-admin-tool"><input type="checkbox" name="isManager" value="true">管理员</label>
              </div>
            </div>
            <div class="uk-grid uk-grid-small h-post-form-grid">
              <div class="uk-width-1-5">
                <div class="h-post-form-title">E-mail</div>
              </div>
              <div class="h-post-form-input uk-width-3-5">
                <input type="text" name="email" size="28" value="" maxlength="100">
              </div>
            </div>
            <div class="uk-grid uk-grid-small h-post-form-grid">
              <div class="uk-width-1-5">
                <div class="h-post-form-title">标题</div>
              </div>
              <div class="h-post-form-input uk-width-3-5">
                <input type="text" name="title" size="28" value="" maxlength="100">
              </div>
              <div class="h-post-form-option uk-width-1-5">
                <input type="submit" value="送出">
              </div>
            </div>
            <!-- 颜文字、正文、附图等 -->
            <div class="uk-grid uk-grid-small h-post-form-grid">
              <div class="uk-width-1-5">
                <div class="h-post-form-title">颜文字</div>
              </div>
              <div class="h-post-form-input uk-width-1-5">
                <select id="h-emot-select">
                ${emotOptions}
                </select>
              </div>
            </div>
            <div class="uk-grid uk-grid-small h-post-form-grid">
              <div class="uk-width-1-5">
                <div class="h-post-form-title h-post-form-textarea-title">正文</div>
              </div>
              <div class="h-post-form-input uk-width-4-5">
                <textarea name="content" maxlength="10000" class="h-post-form-textarea"></textarea>
              </div>
            </div>
            <div class="uk-grid uk-grid-small h-post-form-grid">
              <div class="uk-width-1-5">
                <div class="h-post-form-title">附加图片</div>
              </div>
              <div class="h-post-form-option uk-width-1-6">
                <label class="h-water-tool">
                  <input type="checkbox" name="water" value="true" checked="true">水印
                </label>
              </div>
              <div class="h-post-form-input uk-width-3-5">
                <input type="file" name="image">
              </div>
            </div>
            <!-- 如果页面需要 __hash__ 的实际值,可以在点击串链接并 fetch 后替换此隐藏值 -->
            <!-- 注意:原来 HTML 示例中的 long-hash 在这里用占位 'cirns',后续会覆盖 -->
            <input type="hidden" name="__hash__" value="cirns">
          </form>
          <div class="uk-clearfix"></div>
        </div>
      `;
      // 插入表单:在第一个 hr 下插入表单和一个新的 hr(把表单与列表分离)
      $firstHr.after(formHtml + '<hr>');
      // 返回插入的 form jQuery 对象(方便调用端定位)
      return $('#h-post-form form').first();
      document.dispatchEvent(new Event('timelineReplyFormInserted'));
    }
    // ---------- 根据页面场景获取或插入表单 ----------
    let $formPost = $('form[action="/Home/Forum/doPostThread.html"]').first();   // 板块页发串表单
    let $formReply = $('form[action="/Home/Forum/doReplyThread.html"]').first(); // 串内页回串表单
    // TODO 值班室板块回复表单需要选择原因,无论在板块页快速回复还是串内回复
    if ($formPost.length) {
        // 当前是板块页,直接使用发串表单
    } else if ($formReply.length) {
        // 当前是串内页,直接使用回串表单
    } else if (PageType.isTimelineLikePage()) {
        // 当前是时间线页,无表单 → 插入一个回串表单
        $formReply = $(`
            <form action="/Home/Forum/doReplyThread.html" method="post" id="timeline-reply-form">
                <!-- 这里填入串内页回复表单的必要字段 -->
            </form>
        `);
        $('body').append($formReply);
    }
    if (!$formPost.length && isTimeline) {
      $formPost = bindTimelineReplyForm();
    }
    // 时间线:插入/刷新完表单后,纳入统一的“包裹 + 折叠”
    if (isTimeline && $formPost && $formPost.length) {
      ensureCollapsed($formPost, '『回复』');
    }
    if (!$formPost || !$formPost.length) return;
    // 如果表单中已经存在“回应模式”行(例如我们插入的时间线表单),则复用;否则按原逻辑插入 $row
    let $row = null;
    if ($formPost.find('.h-post-form-title:contains("回应模式")').length) {
      // 优先找到带 js-reply-mode-row 的行(如果插入时已包含)
      $row = $formPost.find('.js-reply-mode-row').first();
    } else {
      // 原有逻辑:找到 名称 行 作为插入位置
      const $nameRow = $formPost.find('.h-post-form-title').filter(function(){
        return $(this).text().trim() === '名 称';
      }).closest('.h-post-form-grid').first();
      if (!$nameRow.length) return;
      // 插入回应模式行(与原代码结构一致)
      $row = $(`
        <div class="uk-grid uk-grid-small h-post-form-grid js-reply-mode-row">
          <div class="uk-width-1-5">
            <div class="h-post-form-title">回应模式</div>
          </div>
          <div class="h-post-form-input uk-width-4-5" style="display:flex;align-items:center;justify-content:space-between;">
            <span class="js-reply-mode-text">板块名或串号</span>
            <span class="xdex-cookie-check-area" style="display:flex;align-items:center;gap:4px;flex-shrink:0;"></span>
            <div class="reply-mode-toggle" style="display:flex;flex-direction:row;align-items:center;gap:6px;">
              <span class="js-reply-extra" style="display:none;display:inline-flex;align-items:center;"></span>
              <button type="button" class="js-toggle-mode xdex-reply-mode-btn" title="当前为&quot;回复&quot;模式,点击切换为&quot;发串&quot;模式">${XDEX_SVG_SWITCH}</button>
            </div>
          </div>
        </div>
      `);
      // 插入到 placeholder 或 名称行之前
      let $insertBefore = $formPost.find('.xdex-placeholder').first();
      if ($insertBefore.length) {
        $insertBefore.before($row);
      } else {
        $nameRow.before($row);
      }
    }
    // 如果当前页面是时间线:不允许发串,隐藏切换按钮并强制设为 回复 模式(但保留临时/连续按钮)
    if (isTimeline) {
      $row.find('.js-toggle-mode').hide();
    }
    // 状态与事件(复用原有逻辑)
    window.replyModeState = { mode: '发串', extra: null };
    function emitReplyModeChange() {
      document.dispatchEvent(new CustomEvent('replyModeChange', { detail: window.replyModeState }));
    }
    // 切换逻辑(保留原版 setMode,外部依赖不变)
    const $modeBtns = $row.find('.js-mode'); // 兼容原代码(若不存在不会报错)
    function setMode(mode, {silent = false} = {}) {
      // 保留用户手动选择的 临时/连续:发串 分支会 .empty() 掉整排按钮并把 extra 置 null,
      // 切回 回复 时按钮 DOM 与 window.replyModeState 都已无值,只能靠 $row 上持久化的值还原,
      // 否则每次 发串↔回复 往返都会把用户的选择打回默认(首帧无历史值 → 仍走默认)
      const prevExtra = $row.find('.js-extra').attr('data-extra')
        || $row.data('xdexLastExtra')
        || (window.replyModeState && window.replyModeState.extra)
        || null;
      $modeBtns.removeClass('active').filter('[data-mode="'+mode+'"]').addClass('active');
      if (mode === '发串') {
      $formPost.attr('action', '/Home/Forum/doPostThread.html');
      $row.find('.js-reply-extra').hide().empty();
      $row.find('.js-reply-mode-text').text(boardBaseName ? (boardBaseName + '-发串') : '板块-发串');
        window.replyModeState = { mode: '发串', extra: null };
        // 发串模式不需要饼干偏好
        clearCookieCheckSwitch();

        if (!silent) {
          toast('已切换到 发串 模式');
        }
      } else if (mode === '回复') {
        $formPost.attr('action', '/Home/Forum/doReplyThread.html');
        // 确保 hidden 存在
        let $resto = $formPost.find('input[name="resto"]');
        if (!$resto.length) {
          $resto = $('<input type="hidden" name="resto">').appendTo($formPost);
        }
        let $hash = $formPost.find('input[name="__hash__"]');
        if (!$hash.length) {
          $hash = $('<input type="hidden" name="__hash__">');
          $resto.after($hash); // 保证顺序
        }
        let autofilled = false;
        if (pendingReplyParams) {
          // 自动填充缓存的参数
          $resto.val(pendingReplyParams.resto);
          $hash.val(pendingReplyParams.hash);
          $row.find('.js-reply-mode-text').html(
            `<font color="#789922">No.${pendingReplyParams.tid}</font>`
          );
          const $replyModeText = $row.find('.js-reply-mode-text');
          // 触发扩展引用
          const root = $replyModeText[0];
          if (typeof initExtendedContent === 'function') { try { initExtendedContent(root); } catch(e){} }
          if (typeof initContent === 'function') { try { initContent(root); } catch(e){} }
          //if (typeof autoHideRefView === 'function') { try { autoHideRefView(root); } catch(e){} }
          toast('已自动填充回复串号,请确认无误后再发送');
          // 串号变化 → 先解析偏好再重建开关,避免开关渲染与偏好脱节
          if (typeof initThreadCookiePref === 'function') initThreadCookiePref(pendingReplyParams.tid);
          if (typeof injectCookieCheckSwitch === 'function') injectCookieCheckSwitch(pendingReplyParams.tid);
          pendingReplyParams = null; // 用过一次就清空
          autofilled = true;
        } else {
          // 默认逻辑
          $resto.val('20011114');
          $hash.val('cirns');
          // 对于时间线,默认显示的文本已经在插入表单时写成 `${timeline}-快速回复`,这里保持不变
          if (!isTimeline) {
            $row.find('.js-reply-mode-text').text((boardBaseName || '板块') + '-快速回复');
          }
          // 无串上下文(切回回复模式但未指定串):开关同样不得残留上一串状态
          clearCookieCheckSwitch();
        }
        // 插入“临时/连续”按钮(若尚未插入)
        const $extra = $row.find('.reply-mode-toggle .js-reply-extra');
        if (!$extra.children().length) {
          // 包裹容器
          const $wrapper = $('<div class="xdex-file-wrapper" style="display:flex;align-items:center;justify-content:space-between;width:100%;"></div>');
          // “×”按钮
          const $btnReset = $('<button type="button" class="js-reset xdex-reply-mode-btn" title="重置回复目标" style="margin-right:6px;">').html(XDEX_SVG_X_ICON);
          $btnReset.on('click', function(){
            // 重置 hidden 值
            $formPost.find('input[name="resto"]').val('20011114');
            $formPost.find('input[name="__hash__"]').val('cirns');
            // 重置显示文本
            if (isTimeline) {
              const timelineNames = {1:'综合线',2:'创作线',3:'非创作线',4:'亚文化线',5:'综合2线',6:'游戏线',7:'生活线'};
              const label = timelineNames[timelineId] || '时间线';
              $row.find('.js-reply-mode-text').text(label + '-快速回复');
            } else {
              $row.find('.js-reply-mode-text').text((boardBaseName || '板块') + '-快速回复');
            }
            // 重置饼干偏好开关
            clearCookieCheckSwitch();

            // **清空正文 textarea**
            $formPost.find('textarea.h-post-form-textarea').val('');
            // **清空标题、名称、Email**
            $formPost.find('input[name="title"]').val('');
            $formPost.find('input[name="name"]').val('');
            $formPost.find('input[name="email"]').val('');
            // 重置预览框
            const previewBox = document.querySelector('.h-preview-box');
            if (previewBox) {
              // 获取当前饼干
              const cur = getCurrentCookie();
              const cookieText = cur ? cur.name : '--';
                previewBox.innerHTML = buildEnhanceIslandPreviewHtml(false);
              if (typeof enableHDImage === 'function') {
                enableHDImage(previewBox);
              }
            }
            updatePreviewCookieId();
            toast('已重置');
          });
          // “临时/连续”按钮
          const $btnExtra = $('<button type="button" class="js-extra xdex-reply-mode-btn" data-extra="临时" title=\'当前为"临时"回复模式,点击切换为"连续"回复模式\'>').html(XDEX_SVG_STOPWATCH);
          $btnExtra.on('click', function(e){
            // 下面会用 .html() 替换被点击的图标本身:若不阻断冒泡,遮罩处理器拿到的 e.target
            // 已脱离文档,closest('.qp-quote') 判定失效,会把这次点击误判为「点了遮罩」而关闭浮窗
            e.stopPropagation();
            const cur = $(this).attr('data-extra');
            if (cur === '临时') {
              $(this).attr('data-extra','连续').html(XDEX_SVG_INFINITY).attr('title','当前为"连续"回复模式,点击切换为"临时"回复模式');
              window.replyModeState = { mode: '回复', extra: '连续' };
              $row.data('xdexLastExtra', '连续'); // 持久化用户选择,供 发串→回复 往返后还原
              toast('已切换到 连续 回复模式');
            } else {
              $(this).attr('data-extra','临时').html(XDEX_SVG_STOPWATCH).attr('title','当前为"临时"回复模式,点击切换为"连续"回复模式');
              window.replyModeState = { mode: '回复', extra: '临时' };
              $row.data('xdexLastExtra', '临时'); // 持久化用户选择,供 发串→回复 往返后还原
              toast('已切换到 临时 回复模式');
            }
            emitReplyModeChange();
          });
          // 组装
          $wrapper.append($btnReset).append($btnExtra);
          $extra.append($wrapper);
        }
        $extra.show();
        // 新建的按钮固定是 临时 图标,这里按 prevExtra 还原(无历史值时保持 临时,再由下方初始状态块应用设置项默认)
        if (prevExtra === '连续') {
          $row.find('.js-extra').attr('data-extra','连续').html(XDEX_SVG_INFINITY).attr('title','当前为"连续"回复模式,点击切换为"临时"回复模式');
        }
        window.replyModeState = { mode: '回复', extra: prevExtra === '连续' ? '连续' : '临时' };
        // 只有在没有自动填充时,才显示默认切换提示
        if (!autofilled && !silent) {
          toast('已切换到 回复 模式');
        }
      }
      // 同步“切换”按钮悬浮提示(setMode 所有路径统一出口)
      {
        const modeName = window.replyModeState.mode === '发串' ? '发串' : '回复';
        const nextName = modeName === '发串' ? '回复' : '发串';
        $row.find('.js-toggle-mode').attr('title', '当前为"' + modeName + '"模式,点击切换为"' + nextName + '"模式');
      }
      emitReplyModeChange();
    }
    // 绑定模式按钮(原先存在的行为)
    $modeBtns.on('click', function(){ setMode($(this).attr('data-mode')); });
    // 记录本行按钮上已有的 临时/连续('临时' 表示用户手动选过;本函数可重复执行,如无缝加载后重跑)
    // 下方三处初始状态分支据此避免用设置项默认值覆盖用户的选择
    const rowPrevExtra = $row.find('.js-extra').attr('data-extra') || $row.data('xdexLastExtra') || null;
    // 初始状态:如果是时间线则强制 回复 模式(silent),否则默认静默发串
    if (isTimeline) {
      setMode('回复', {silent: true});
      // extra 模式(rowPrevExtra 为 '临时' 说明用户手动选过,不再用设置项默认覆盖)
      if (SettingPanel.state.replyExtraDefault === '连续' && rowPrevExtra !== '临时') {
        // 模拟点击一次“临时/连续”按钮,或者直接设置
        window.replyModeState.extra = '连续';
        $row.find('.js-extra').attr('data-extra','连续').html(XDEX_SVG_INFINITY).attr('title','当前为"连续"回复模式,点击切换为"临时"回复模式');
      }
    } else if (PageType.isSpecialBoard()) {
      // 值班室/测试版块强制发串模式,方便快速举报/测试(不修改设置项,用户仍可手动切换回复模式)
      const _boardName = (/^\/f\/(值班室|测试)(?:\/|$)/.exec(decodeURIComponent(location.pathname)) || [])[1] || '特殊';
      setMode('发串', {silent: true});
      try {
        const today = new Date().toLocaleDateString('sv-SE');
        const toastKey = 'xdex_' + _boardName + '_post_mode_toast_date';
        const lastShown = typeof GM_getValue === 'function' ? GM_getValue(toastKey, '') : '';
        if (lastShown !== today) {
          toast(_boardName + '版块默认为"发串"模式,请注意', 0, { type: 'error', key: 'board-post-mode-notice' });
          if (typeof GM_setValue === 'function') GM_setValue(toastKey, today);
        }
      } catch (e) {
        toast(_boardName + '版块默认为"发串"模式,请注意', 0, { type: 'error', key: 'board-post-mode-notice' });
      }
      if (SettingPanel.state.replyExtraDefault === '连续' && rowPrevExtra !== '临时') {
        window.replyModeState.extra = '连续';
        $row.find('.js-extra').attr('data-extra','连续').html(XDEX_SVG_INFINITY).attr('title','当前为"连续"回复模式,点击切换为"临时"回复模式');
      }
    } else {
      setMode(SettingPanel.state.replyModeDefault, {silent: true});
      // extra 模式(rowPrevExtra 为 '临时' 说明用户手动选过,不再用设置项默认覆盖)
      if (SettingPanel.state.replyExtraDefault === '连续' && rowPrevExtra !== '临时') {
        window.replyModeState.extra = '连续';
        $row.find('.js-extra').attr('data-extra','连续').html(XDEX_SVG_INFINITY).attr('title','当前为"连续"回复模式,点击切换为"临时"回复模式');
      }
    }
    // 切换按钮逻辑(若存在切换按钮)
    $row.find('.js-toggle-mode').on('click', function(){
      if (window.replyModeState.mode === '发串') {
        setMode('回复');
      } else {
        setMode('发串');
      }
    });
    // 监听发送成功信号(兼容原逻辑)
    document.addEventListener('replySuccess', e => {
      if (window.replyModeState?.mode !== '回复') return;
      const $form = $('form[action="/Home/Forum/doReplyThread.html"]').first();
      if (window.replyModeState.extra === '临时') {
        // 重置为默认
        $form.find('input[name="resto"]').val('20011114');
        $form.find('input[name="__hash__"]').val('cirns');
        const displayName = isTimeline
          ? (timelineNameMap[timelineId] || '时间线')
          : (boardBaseName || '板块');
        $('.js-reply-mode-row .js-reply-mode-text').text(`${displayName}-快速回复`);
        // 串上下文已清除,饼干偏好开关必须一并清空:否则残留上一串的默认饼干状态与齿轮
        clearCookieCheckSwitch();
        // 广播“临时回复发送成功”
        document.dispatchEvent(new CustomEvent('tempReplySuccess', {
          detail: { key: e.detail?.key, tid: e.detail?.tid }
        }));
      } else if (window.replyModeState.extra === '连续') {
        // 不清理,广播“连续回复发送成功”
        document.dispatchEvent(new CustomEvent('contReplySuccess', {
          detail: { key: e.detail?.key, tid: e.detail?.tid }
        }));
      }
    });
    // ---------- 子函数2:处理带 r= 的串链接点击(复用并兼容时间线插入的表单) ----------
    function bindReplyQuoteLinks() {
      $('body').on('click', 'a[href*="/t/"][href*="r="]', function(e) {
        if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
        const url = new URL(this.href, location.origin);
        const tid = url.pathname.split('/')[2];
        const rid = url.searchParams.get('r');
        currentReplyTid = tid;
        if (!tid || !rid) return;
        e.preventDefault();
        const $textarea = $('textarea.h-post-form-textarea');
        if (!$textarea.length) return;
        if (window.replyModeState?.mode === '发串') {
          insertQuote($textarea, rid);
          // 抓取参数并缓存(用于后续切换到 回复 模式 自动填充)
          $.get(`/t/${tid}?r=${rid}`).done(html => {
            const $doc = $(html);
            const restoVal = $doc.find('form[action="/Home/Forum/doReplyThread.html"] input[name="resto"]').val();
            const hashVal  = $doc.find('form[action="/Home/Forum/doReplyThread.html"] input[name="__hash__"]').val();
            if (restoVal && hashVal) {
              pendingReplyParams = { tid, resto: restoVal, hash: hashVal };
            }
          });
        }
        else {
          $.get(`/t/${tid}?r=${rid}`).done(html => {
            const $doc = $(html);
            const restoVal = $doc.find('form[action="/Home/Forum/doReplyThread.html"] input[name="resto"]').val();
            const hashVal  = $doc.find('form[action="/Home/Forum/doReplyThread.html"] input[name="__hash__"]').val();
            if (restoVal && hashVal) {
              const $form = $('form[action="/Home/Forum/doReplyThread.html"]').first();
              $form.find('input[name="resto"]').val(restoVal);
              $form.find('input[name="__hash__"]').val(hashVal);
              // 在回应模式行中显示串号(直接插入 font)
              const $replyModeText = $('.js-reply-mode-row .js-reply-mode-text');
              $replyModeText.html(
                `<font color="#789922" data-darkreader-inline-color="" style="--darkreader-inline-color: var(--darkreader-text-789922, #aec66f);">No.${tid}</font>`
              );

              // 串号变化 → 先解析偏好再重建开关,避免开关渲染与偏好脱节
              if (typeof initThreadCookiePref === 'function') initThreadCookiePref(tid);
              if (typeof injectCookieCheckSwitch === 'function') injectCookieCheckSwitch(tid);

              const root = $replyModeText[0];
              if (typeof initExtendedContent === 'function') { try { initExtendedContent(root); } catch(e){} }
              if (typeof initContent === 'function') { try { initContent(root); } catch(e){} }
              //if (typeof autoHideRefView === 'function') { try { autoHideRefView(root); } catch(e){} }
            }
            if (tid !== rid) {
              insertQuote($textarea, rid);
            }
          });
        }
      });
      function insertQuote($textarea, rid) {
        // 手动追记:放行关闭引用拦截,避免 >>No.{r} 被剥掉
        if (typeof allowManualQuoteInsert === 'function') allowManualQuoteInsert(rid);
        const start = $textarea.prop('selectionStart');
        const end   = $textarea.prop('selectionEnd');
        const str   = $textarea.val();
        const left  = str.substring(0, start);
        const right = str.substring(end);
        const ref   = `>>No.${rid}`;
        let newVal;
        if (start === 0) {
          newVal = `${ref}\n${right}`;
        } else if (end === str.length) {
          newVal = `${left}\n${ref}\n`;
        } else if (end > start) {
          newVal = `${left} ${ref} ${right}`;
        } else {
          newVal = `${left}\n${ref}${right}`;
        }
        // 旧:$textarea.val(newVal).trigger('input');
        // 同步派发原生 input,保证字数统计即时刷新
        $textarea.val(newVal).trigger('input');
        const taEl = $textarea[0];
        if (taEl) taEl.dispatchEvent(new Event('input', { bubbles: true }));
      }
    }
    // ---------- 公用:重新应用页面增强(保持原样) ----------
    // function reapplyPageEnhancements(root = document) {
    //   if (typeof hideEmptyTitleAndEmail === 'function') {try { hideEmptyTitleAndEmail(root); } catch (e) {}}
    //   //if (typeof updateReplyNumbers === 'function') {try { updateReplyNumbers(root); } catch (e) {}}
    //   if (typeof highlightPO === 'function') {try { highlightPO(root); } catch (e) {}}
    //   if (typeof enableHDImageAndLayoutFix === 'function') {try { enableHDImageAndLayoutFix(root); } catch (e) {}}
    //   if (typeof extendQuote === 'function') {try { extendQuote(root); } catch (e) {}}
    //   try { if (typeof applyFilters === 'function') applyFilters(cfg, root); } catch (e) { try { if (typeof applyFilters === 'function') applyFilters(cfg); } catch (e) {} }
    //   if (typeof initExtendedContent === 'function') {try { initExtendedContent(root); } catch (e) {}}
    //   if (typeof initContent === 'function') {try { initContent(root); } catch (e) {}}
    //   if (typeof autoHideRefView === 'function') {try { autoHideRefView(root); } catch (e) {}}
    //   if (typeof enablePostExpand === 'function') {try { aenablePostExpand(); } catch (e) {}}
    // }
    // ---------- 子函数3:板块/时间线快速回复刷新逻辑(已扩展以支持时间线) ----------
    // done 将板块页快速回复-局部刷新修改为增量模式
    function bindBoardQuickReplyRefresh() {
      document.addEventListener('tempReplySuccess', handleBoardQuickReplyRefresh);
      document.addEventListener('contReplySuccess', handleBoardQuickReplyRefresh);
      document.addEventListener('xdexBoardThreadRefresh', handleBoardQuickReplyRefresh);
      // 按钮触发的刷新:同串并发去重(回复后的自动增量不受影响)
      const _boardRefreshInflight = new Set();
      // === v3: API 拉取末页+倒数第二页,位置感知增量合并,滚动位置保持 ===
      function handleBoardQuickReplyRefresh(e) {
        // 只在 板块页 或 时间线 页生效
        if (!PageType.isBoardPage() && !PageType.isTimelinePage()) return;
        const btn = (e.detail && e.detail.btn) || null;
        const fromButton = !!(e.detail && e.detail.fromButton);
        const tid = e.detail?.tid || currentReplyTid;
        if (!tid) {
          toast('订阅失败:未识别到当前串号');
          return;
        }
        if (fromButton) {
          if (_boardRefreshInflight.has(String(tid))) return;
          _boardRefreshInflight.add(String(tid));
          if (btn) { btn.disabled = true; btn.classList.add('xdex-icon-loading'); }
        }
        const cfg2 = (typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state)
          ? SettingPanel.state : null;
        const REPLY_PER_PAGE = 19;
        const API_BASE = 'https://api.nmb.best/api';
        // ★ 整链计时:板块页/时间线发送后增量刷新(B 场景)。
        // 注意用 begin/finish 而非 measure:本函数是 async,measure 只能量到首个 await 之前,
        // 真正的同步 DOM 工作(Step 5-8)在多个 await 之后的续体里。
        const __perfToken = startupPerfDebug.begin();
        (async () => {
          try {
            // Step 1: 拉取第1页获取 ReplyCount → 计算末页页码
            // ★ 逐段计时:整链 7.5s 而同步 DOM 仅 ~28ms,必须确认耗时是网络还是「桥接阻塞」
            const __tA1 = startupPerfDebug.begin();
            const p1Resp = await gmRequest(API_BASE + '/thread?id=' + encodeURIComponent(tid) + '&page=1', 'json');
            startupPerfDebug.finish('boardQuickReply.await.p1', __tA1, () => ({}));
            const p1Raw = p1Resp && (p1Resp.response || p1Resp.responseText);
            const p1 = typeof p1Raw === 'string' ? JSON.parse(p1Raw) : p1Raw;
            if (!p1 || p1.success === false) throw new Error((p1 && p1.error) || 'API error');
            const replyCount = Number(p1.ReplyCount || p1.reply_count || 0);
            // OP 主的饼干哈希: 用于增量插入回复时补插 (PO主) 标记
            const opUserHash = String((p1 && (p1.user_hash || p1.UserHash)) || '');
            const tailPage = Math.max(1, Math.ceil(replyCount / REPLY_PER_PAGE));
            // Step 2: 拉取末页
            const __tA2 = startupPerfDebug.begin();
            const tResp = await gmRequest(API_BASE + '/thread?id=' + encodeURIComponent(tid) + '&page=' + tailPage, 'json');
            startupPerfDebug.finish('boardQuickReply.await.tail', __tA2, () => ({ page: tailPage }));
            const tRaw = tResp && (tResp.response || tResp.responseText);
            const t = typeof tRaw === 'string' ? JSON.parse(tRaw) : tRaw;
            if (!t || t.success === false) throw new Error((t && t.error) || 'API error');
            let tailReplies = (Array.isArray(t.Replies) ? t.Replies : []).filter(r => r && Number(r.id) !== 9999999);
            // ★ 借道回填:本次请求的数据已含刚发回复与 thread 顶层 fid——立即补齐
            // pending 发言历史记录的 fid/forumName/page/title(零新增请求,字段即刻安全)
            try { enrichPendingPostHistoryFromThreadData(t, tailReplies, tailPage); } catch (e) {}
            // Step 3: 定位页面上**所有**该串节点(无缝翻页后同一串可能出现在多个 .h-threads-list 中)
            const allOldNodes = Array.from(document.querySelectorAll('.h-threads-list div[data-threads-id="' + tid + '"]'));
            if (!allOldNodes.length) return;
            // 用第一个节点的最后一条回复 ID 作为 API 数据范围的基准
            const firstNode = allOldNodes[0];
            const firstReplyEls = Array.from(firstNode.querySelectorAll('.h-threads-item-reply[data-threads-id]'));
            const lastOldId = firstReplyEls.length ? Number(firstReplyEls[firstReplyEls.length - 1].getAttribute('data-threads-id')) : 0;
            // Step 4: 定位 lastOldId 并确定新回复范围
            // 关键:板块页展示的最近5条回复可能跨页(如 [18,19] 在倒数第二页,[1,2,3] 在末页)
            // 如果 lastOldId 不在末页,必须同时拉取倒数第二页,避免漏掉中间的回复
            let newReplies = [];
            if (lastOldId > 0) {
              const idx = tailReplies.findIndex(r => r && Number(r.id) === lastOldId);
              if (idx >= 0) {
                // lastOldId 在末页中 → 取其后的所有回复
                newReplies = tailReplies.slice(idx + 1);
              } else {
                // lastOldId 不在末页 → 需要拉倒数第二页填补间隙
                if (tailPage > 1) {
                  const __tA3 = startupPerfDebug.begin();
                  const pResp = await gmRequest(API_BASE + '/thread?id=' + encodeURIComponent(tid) + '&page=' + (tailPage - 1), 'json');
                  startupPerfDebug.finish('boardQuickReply.await.prev', __tA3, () => ({ page: tailPage - 1 }));
                  const pRaw = pResp && (pResp.response || pResp.responseText);
                  const p = typeof pRaw === 'string' ? JSON.parse(pRaw) : pRaw;
                  const prevReplies = (Array.isArray(p && p.Replies) ? p.Replies : []).filter(r => r && Number(r.id) !== 9999999);
                  // 合并两页:倒数第二页 + 末页
                  const merged = prevReplies.concat(tailReplies);
                  const mIdx = merged.findIndex(r => r && Number(r.id) === lastOldId);
                  if (mIdx >= 0) {
                    newReplies = merged.slice(mIdx + 1);
                  } else {
                    // 两页数据里都找不到页面上的最后一条回复 → 用户看到的页面数据太老
                    toast('当前页面数据过旧,请手动刷新页面');
                    return;
                  }
                } else {
                  // 只有一页且找不到 lastOldId → 页面数据过旧
                  if (tailReplies.some(r => r && Number(r.id) === lastOldId)) {
                    const sIdx = tailReplies.findIndex(r => r && Number(r.id) === lastOldId);
                    newReplies = tailReplies.slice(sIdx + 1);
                  } else {
                    toast('当前页面数据过旧,请手动刷新页面');
                    return;
                  }
                }
              }
            } else {
              // 无回复区串:页面从未展示过回复;排除 PO 主帖(Replies[0].id === tid)后即为新增回复
              newReplies = tailReplies.filter(r => r && Number(r.id) !== Number(tid));
            }
            if (!newReplies.length) { if (fromButton) toast('已是最新回复'); return; }
            // Step 5: 保存滚动位置(插入前)
            // ★ 强制同步布局热点:每个 node 读一次 getBoundingClientRect + offsetHeight
            const __t5 = startupPerfDebug.begin();
            const scrollEl = document.scrollingElement || document.documentElement;
            const scrollTopBefore = scrollEl.scrollTop;
            const viewportTop = scrollTopBefore;
            // 记录每个节点插入前的 offsetTop 和高度(用于滚动补偿)
            const nodeMetricsBefore = allOldNodes.map(node => {
              const rect = node.getBoundingClientRect();
              return {
                node,
                bottom: rect.bottom,
                height: node.offsetHeight
              };
            });
            startupPerfDebug.finish('boardQuickReply.step5.measure', __t5, () => ({ nodes: allOldNodes.length }));
            // Step 6: 遍历所有匹配节点,每个节点独立增量追加
            // ★ 每次 appendChild 都会触发 document.body 子树的多个观察器(hdGlobal 等),
            // 插入 N 条 = N 轮全页观察器回调,是「发送后卡顿」的首要嫌疑
            const __t6 = startupPerfDebug.begin();
            let __inserted = 0;
            const appendedNodes = [];
            for (const node of allOldNodes) {
              let rc = node.querySelector('.h-threads-item-replies');
              // 无回复区(新串未展开过回复)→ 创建容器,否则增量刷新会被静默跳过
              if (!rc) {
                rc = document.createElement('div');
                rc.className = 'h-threads-item-replies';
                node.appendChild(rc);
              }
              // 收集该节点已有的回复 ID
              const existingIds = new Set(
                Array.from(rc.querySelectorAll('.h-threads-item-reply[data-threads-id]'))
                  .map(el => el.getAttribute('data-threads-id'))
                  .filter(id => id && id !== '9999999')
              );
              // 只追加该节点缺失的回复
              for (const reply of newReplies) {
                if (!existingIds.has(String(reply.id))) {
                  const el = buildApiReplyNode(reply, tid, opUserHash);
                  if (el) { rc.appendChild(el); appendedNodes.push(el); __inserted++; }
                }
              }
            }
            startupPerfDebug.finish('boardQuickReply.step6.insert', __t6, () => ({ nodes: allOldNodes.length, inserted: __inserted }));
            // Step 7: 计算视口上方节点增长的高度,补偿滚动位置
            // ★ 又一次强制同步布局:读取每个 node 的 offsetHeight + 写 scrollTop
            const __t7 = startupPerfDebug.begin();
            // 只有完全在视口上方(bottom <= viewportTop)的节点增长才需要补偿
            let heightAddedAbove = 0;
            for (let i = 0; i < allOldNodes.length; i++) {
              const before = nodeMetricsBefore[i];
              const afterHeight = allOldNodes[i].offsetHeight;
              const delta = afterHeight - before.height;
              if (delta > 0 && before.bottom <= viewportTop) {
                heightAddedAbove += delta;
              }
            }
            scrollEl.scrollTop = scrollTopBefore + heightAddedAbove;
            startupPerfDebug.finish('boardQuickReply.step7.compensate', __t7, () => ({ nodes: allOldNodes.length, addedAbove: heightAddedAbove }));
            // Step 8: 后处理增强(所有匹配节点)
            const __t8 = startupPerfDebug.begin();
            for (const node of allOldNodes) {
              try { if (typeof hideEmptyTitleAndEmail === 'function') hideEmptyTitleAndEmail($(node)); } catch (err) {}
              try { if (typeof markAllCookies === 'function') markAllCookies(getFilterConfig().markedGroups || [], node); } catch (err) {}
            }
            startupPerfDebug.finish('boardQuickReply.step8.post', __t8, () => ({ nodes: allOldNodes.length }));
            // 延迟执行其他增强
            setTimeout(() => {
              // ★ deferred 只处理本次新增的回复(appendedNodes):
              // 旧回复在首载/前次刷新已处理过(各函数幂等锁保证不重复)。
              // 原实现遍历 allOldNodes(整条串)→ enableHDImageAndLayoutFix 对整串所有元素
              // 读布局+写样式(getBoundingClientRect×每元素,读 写 读 交错强制重排),
              // 再叠加 refreshFilterDisplay 全文档过滤重算——「插入后卡顿」的主源。
              // 对齐串内页行为(applyPageEnhancements 只处理新增内容)。
              if (!appendedNodes.length) return;
              const __td = startupPerfDebug.begin();
              // ── A 组:需串级作用域(highlightPO 的楼层编号要遍历 .h-threads-item-replies 容器,
              //    以单条新回复为 scope 会空转 → 新回复无编号/无 Po 标,实测截图问题)。
              //    highlightPO 是纯 DOM 文本/样式写、无布局读,对整串跑成本低。
              for (const node of allOldNodes) {
                try { if (typeof highlightPO === 'function') highlightPO(node); } catch (err) {}
              }
              // ── B 组:布局读写热点,严格限定本次新增回复(旧回复不再重跑——
              //    enableHDImageAndLayoutFix 的 handleGeneralElements 对每元素
              //    读布局+写样式交错,整串重跑会强制重排数百次 = 插入后卡顿主源)
              for (const node of appendedNodes) {
                const replyScope = node.closest('.h-threads-item-reply') || node;
                try { if (cfg2 && cfg2.enableHDImageAndLayoutFix && typeof enableHDImageAndLayoutFix === 'function') enableHDImageAndLayoutFix(replyScope); } catch (err) {}
                try { if (cfg2 && cfg2.enableHDImage && typeof enableHDImage === 'function') enableHDImage(replyScope); } catch (err) {}
                try { if (cfg2 && cfg2.enableLinkBlank && typeof runLinkBlank === 'function') runLinkBlank(node); } catch (err) {}
                try { if (cfg2 && cfg2.extendQuote && typeof extendQuote === 'function') extendQuote(node); } catch (err) {}
                try { if (typeof initContent === 'function') initContent(node); } catch (err) {}
                try { if (typeof initExtendedContent === 'function') initExtendedContent(node); } catch (err) {}
              }
              // ── D 组:过滤与展开按钮按串节点跑(轻量 DOM 遍历/样式写,无布局读;
              //    新回复的过滤结果可能影响串的展开/折叠形态)
              const __tf = startupPerfDebug.begin();
              for (const node of allOldNodes) {
                try { if (cfg2) refreshFilterDisplay(cfg2, node); } catch (err) {}
              }
              startupPerfDebug.finish('boardQuickReply.deferred.refreshFilter', __tf, () => ({ nodes: allOldNodes.length }));
              const __tp = startupPerfDebug.begin();
              for (const node of allOldNodes) {
                try { if (typeof enablePostExpand === 'function') enablePostExpand(node); } catch (err) {}
              }
              startupPerfDebug.finish('boardQuickReply.deferred.enablePostExpand', __tp, () => ({ nodes: allOldNodes.length }));
              startupPerfDebug.finish('boardQuickReply.deferred.total', __td, () => ({ nodes: appendedNodes.length }));
            }, 50);
            if (fromButton) toast('已更新 ' + newReplies.length + ' 条回复');
            if (e.type === 'tempReplySuccess') currentReplyTid = null;
          } catch (err) {
            console.warn('[board-quick-reply] API refresh failed', err);
            toast('刷新板块串失败');
          }
        })().finally(() => {
          // ★ 整链收尾:含全部 await 等待与 Step 5-8 同步工作;与 LoAF 的 blockingDuration 对照
          startupPerfDebug.finish('boardQuickReply.total', __perfToken, () => ({ fromButton }));
          if (btn) { btn.disabled = false; btn.classList.remove('xdex-icon-loading'); }
          if (fromButton) _boardRefreshInflight.delete(String(tid));
        });
      }
      // 构建 API 回复的 DOM 节点(匹配页面原生 .h-threads-item-reply 结构)
      function buildApiReplyNode(reply, threadId, opUserHash) {
        if (!reply || !reply.id) return null;
        const _e = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
        const id = String(reply.id);
        const title = String(reply.title || '');
        const email = String(reply.email || '');
        const now = String(reply.now || '');
        const hash = String(reply.user_hash || '');
        const contentHtml = String(reply.content || '');
        const imgRaw = String(reply.img || '');
        const extRaw = String(reply.ext || '');
        const suf = extRaw ? (extRaw[0] === '.' ? extRaw : '.' + extRaw) : '';
        const fullUrl = imgRaw ? 'https://image.nmb.best/image/' + imgRaw + suf : '';
        const thumbUrl = imgRaw ? 'https://image.nmb.best/thumb/' + imgRaw + suf : '';
        // 图片区(含工具栏)
        const imgHtml = imgRaw
          ? '<div class="h-threads-img-box">'
            + '<div class="h-threads-img-tool uk-animation-slide-top">'
            +   '<span class="h-threads-img-tool-btn h-threads-img-tool-small uk-button-link"><i class="uk-icon-minus"></i>收起</span>'
            +   '<a href="' + _e(fullUrl) + '" target="_blank" class="h-threads-img-tool-btn uk-button-link"><i class="uk-icon-search-plus"></i>查看大图</a>'
            +   '<span class="h-threads-img-tool-btn h-threads-img-tool-left uk-button-link"><i class="uk-icon-reply"></i>向左旋转</span>'
            +   '<span class="h-threads-img-tool-btn h-threads-img-tool-right uk-button-link"><i class="uk-icon-share"></i>向右旋转</span>'
            + '</div>'
            + '<a href="' + _e(fullUrl) + '" rel="_blank" target="_blank" class="h-threads-img-a">'
            +   '<img data-src="' + _e(thumbUrl) + '" src="' + _e(thumbUrl) + '" align="left" border="0" hspace="20" class="h-threads-img">'
            + '</a>'
          + '</div>'
          : '';
        // 信息区
        const uidHtml = hash
          ? '<span class="h-threads-info-uid" data-xdex-cookie-id="' + _e(hash) + '">ID:' + _e(hash) + '</span>'
          : '';
        // PO 主判定: 与串的 OP 饼干哈希一致时插入 (PO主) 标记, 供 highlightPO 识别替换 Po 图标
        const poMarkHtml = (opUserHash && hash && hash === opUserHash)
          ? ' <span class="uk-text-primary uk-text-small">(PO主)</span>'
          : '';
        const titleText = (title && title !== '无标题') ? _e(title) : '无标题';
        const emailText = (email && email !== '无名氏') ? _e(email) : '无名氏';
        const html = '<div data-threads-id="' + _e(id) + '" class="h-threads-item-reply">'
          + '<div class="h-threads-item-reply-icon">…</div>'
          + '<div class="h-threads-item-reply-main">'
            + imgHtml
            + '<div class="h-threads-info">'
              + '<span class="h-threads-info-title">' + titleText + '</span>'
              + '<span class="h-threads-info-email">' + emailText + '</span>'
              + '<span class="h-threads-info-createdat">' + _e(now) + '</span>'
              + uidHtml
              + poMarkHtml
              + '<span class="h-threads-info-report-btn">[<a href="/f/值班室?r=' + _e(id) + '">举报</a>]</span>'
              + '<a href="/t/' + _e(threadId) + '?r=' + _e(id) + '" class="h-threads-info-id">No.' + _e(id) + '</a>'
            + '</div>'
            + '<div class="h-threads-content">' + contentHtml + '</div>'
          + '</div>'
        + '</div>';
        // ★ 逐条计时:innerHTML 解析成本。Step 6 内循环调用,条数多时累加可观
        const __tok = startupPerfDebug.begin();
        const tmp = document.createElement('div');
        tmp.innerHTML = html;
        startupPerfDebug.finish('boardQuickReply.buildNode', __tok, () => ({ html: html.length, hasImg: !!imgRaw }));
        return tmp.firstChild || null;
      }
    }
    // 统一调用
    bindReplyQuoteLinks();
    bindBoardQuickReplyRefresh();
    bindBoardThreadRefreshButtons(document);
  }
  // tag 15 子功能:板块页/时间线串卡片「获取最新回复」按钮
  // 复用 handleBoardQuickReplyRefresh 的增量刷新;放在展开/收起按钮(.js-toggle-mode)之后
  function bindBoardThreadRefreshButtons(root) {
    if (!PageType.isBoardPage() && !PageType.isTimelinePage()) return;
    const scanRoot = (root && typeof root.querySelectorAll === 'function') ? root : document;
    const items = [];
    if (scanRoot.classList && scanRoot.classList.contains('h-threads-item-index')) items.push(scanRoot);
    scanRoot.querySelectorAll('.h-threads-item-index').forEach(it => { if (items.indexOf(it) < 0) items.push(it); });
    items.forEach(item => {
      const infoBar = item.querySelector('.h-threads-info');
      if (!infoBar) return;
      let tid = item.getAttribute('data-threads-id');
      if (!tid) {
        const idEl = infoBar.querySelector('.h-threads-info-id');
        const m = idEl && (idEl.textContent || '').match(/\d+/);
        if (m) tid = m[0];
      }
      if (!tid) return;
      let btn = infoBar.querySelector(':scope > .xdex-board-refresh-btn');
      if (!btn) {
        btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'xdex-icon-btn xdex-board-refresh-btn';
        btn.title = '获取最新回复';
        btn.style.cssText = 'min-width:1em;text-align:center;margin-left:4px;vertical-align:middle;';
        btn.setAttribute('data-tid', tid);
        btn.innerHTML = XDEX_ICON_SYNC;
        btn.addEventListener('click', function (ev) {
          ev.preventDefault(); ev.stopPropagation();
          const t = btn.getAttribute('data-tid');
          if (!t) return;
          document.dispatchEvent(new CustomEvent('xdexBoardThreadRefresh', { detail: { tid: t, fromButton: true, btn: btn } }));
        });
        infoBar.appendChild(btn);
      } else {
        btn.setAttribute('data-tid', tid);
      }
      // 定位到展开/收起按钮之前(本按钮加载更快,置于左侧;enablePostExpand 可能晚于本函数执行,故每次重排)
      const toggle = infoBar.querySelector(':scope > .js-toggle-mode');
      if (toggle && toggle.previousElementSibling !== btn) toggle.insertAdjacentElement('beforebegin', btn);
    });
  }
  // === 通用:确保某个元素被折叠(幂等) ===
  function ensureCollapsed($elem, hint) {
    if (!$elem || !$elem.length) return;
    if ($elem.data('xdex-collapsed')) return; // 已折叠过
    Utils.collapse($elem, hint);
  }
  const postExpandDocScroller = document.scrollingElement || document.documentElement;

  function getScrollContainer(startEl) {
    let el = startEl || document.body;
    while (el && el !== document.body) {
      const style = getComputedStyle(el);
      const canScroll = /(auto|scroll)/.test(style.overflowY) && el.scrollHeight > el.clientHeight;
      if (canScroll) return el;
      el = el.parentElement;
    }
    return postExpandDocScroller;
  }
  function getViewportRect(scroller) {
    if (scroller === postExpandDocScroller || scroller === document.body || scroller === document.documentElement) {
      return { top: 0, bottom: window.innerHeight };
    }
    const r = scroller.getBoundingClientRect();
    return { top: r.top, bottom: r.bottom };
  }
  function isPartiallyInViewport(el, scroller) {
    if (!el) return false;
    const er = el.getBoundingClientRect();
    const vr = getViewportRect(scroller);
    const visibleTop = Math.max(er.top, vr.top);
    const visibleBottom = Math.min(er.bottom, vr.bottom);
    return Math.max(0, visibleBottom - visibleTop) > 0;
  }
  function getElementAbsTop(el, scroller) {
    const er = el.getBoundingClientRect();
    if (scroller === postExpandDocScroller || scroller === document.body || scroller === document.documentElement) {
      return er.top + window.scrollY;
    }
    const sr = scroller.getBoundingClientRect();
    return er.top - sr.top + scroller.scrollTop;
  }
  function scrollByCompensation(scroller, dy) {
    if (!dy) return;
    if (scroller === postExpandDocScroller || scroller === document.body || scroller === document.documentElement) {
      window.scrollBy({ top: dy, left: 0, behavior: 'auto' });
    } else {
      scroller.scrollTop += dy;
    }
  }
  function findNextThread(item) {
    let el = item.nextElementSibling;
    while (el) {
      if (el.classList && el.classList.contains('h-threads-item-index')) return el;
      el = el.nextElementSibling;
    }
    return null;
  }
  function collapseWithoutShift(item) {
    const postForm = document.getElementById('h-post-form');
    if (postForm && isPartiallyInViewport(postForm, postExpandDocScroller)) {
      item.classList.remove('expanded');
      return;
    }
    if (item.classList.contains('qp-reply-form') ||
        item.classList.contains('xdex-generic-collapsed') ||
        item.closest('.xdex-generic-toggle')) {
      item.classList.remove('expanded');
      return;
    }
    const scroller = getScrollContainer(item);
    const scrollerIsDoc = (scroller === postExpandDocScroller || scroller === document.body || scroller === document.documentElement);
    const next = findNextThread(item);
    const nextVisible = isPartiallyInViewport(next, scroller);
    const threadContainer = scrollerIsDoc ? document : scroller;
    const allThreadsInScroller = Array.from(threadContainer.querySelectorAll('.h-threads-item-index'));
    const visibleThreads = allThreadsInScroller.filter(t => isPartiallyInViewport(t, scroller));
    if (visibleThreads.length === 1 && visibleThreads[0] === item && next) {
      item.classList.remove('expanded');
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          try {
            const vr = getViewportRect(scroller);
            const vHeight = Math.max(1, vr.bottom - vr.top);
            const currentScroll = scrollerIsDoc ? window.scrollY : scroller.scrollTop;
            const nextAbsTop = getElementAbsTop(next, scroller);
            const desiredAbsTop = currentScroll + Math.floor(vHeight / 3);
            const delta = nextAbsTop - desiredAbsTop;
            if (delta !== 0) scrollByCompensation(scroller, delta);
          } catch (err) {
            try {
              const preScroll = scrollerIsDoc ? window.scrollY : scroller.scrollTop;
              const preHeight = scroller.scrollHeight;
              requestAnimationFrame(() => {
                const postHeight = scroller.scrollHeight;
                const d = preHeight - postHeight;
                if (d > 0) {
                  const target = Math.max(0, preScroll - d);
                  if (scrollerIsDoc) window.scrollTo({ top: target, left: 0, behavior: 'auto' });
                  else scroller.scrollTop = target;
                }
              });
            } catch (e2) {
              console.warn('collapseWithoutShift special-case fallback 异常:', e2);
            }
          }
        });
      });
      return;
    }
    if (nextVisible) {
      const preScroll = scrollerIsDoc ? window.scrollY : scroller.scrollTop;
      const preHeight = scroller.scrollHeight;
      item.classList.remove('expanded');
      requestAnimationFrame(() => {
        const postHeight = scroller.scrollHeight;
        const delta = preHeight - postHeight;
        if (delta > 0) {
          const target = Math.max(0, preScroll - delta);
          if (scrollerIsDoc) window.scrollTo({ top: target, left: 0, behavior: 'auto' });
          else scroller.scrollTop = target;
        }
      });
      return;
    }
    const anchor = item;
    const preAbsTop = getElementAbsTop(anchor, scroller);
    item.classList.remove('expanded');
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        const postAbsTop = getElementAbsTop(anchor, scroller);
        const delta = preAbsTop - postAbsTop;
        if (delta !== 0) {
          scrollByCompensation(scroller, delta);
        } else {
          try {
            const preScroll = scrollerIsDoc ? window.scrollY : scroller.scrollTop;
            const preHeight = scroller.scrollHeight;
            requestAnimationFrame(() => {
              const postHeight = scroller.scrollHeight;
              const d = preHeight - postHeight;
              if (d > 0) {
                const target = Math.max(0, preScroll - d);
                if (scrollerIsDoc) window.scrollTo({ top: target, left: 0, behavior: 'auto' });
                else scroller.scrollTop = target;
              }
            });
          } catch (err) {
            console.warn('collapseWithoutShift fallback 异常:', err);
          }
        }
      });
    });
  }

  /* --------------------------------------------------
   * tag 16. 允许展开/收起被板块页折叠的长串
   * -------------------------------------------------- */
  function enablePostExpand(root = document) {
    const scanRoot = root && typeof root.querySelectorAll === 'function' ? root : document;
    const scanTargets = [];
    if (scanRoot.classList && scanRoot.classList.contains('h-threads-item-index')) {
      scanTargets.push(scanRoot);
    }
    scanRoot.querySelectorAll('.h-threads-item-index').forEach(item => {
      if (!scanTargets.includes(item)) scanTargets.push(item);
    });
    // 注入样式(同前,保留 overflow-anchor: none)
    (function ensureExpandedStyle() {
      const id = 'h-expanded-style';
      if (document.getElementById(id)) return;
      const style = document.createElement('style');
      style.id = id;
      style.textContent = `
        html, body {
          overflow-y: scroll !important;
        }
        .h-threads-item-index.expanded,
        html.xdex-post-expand-all .h-threads-item-index:not(.xdex-post-expand-collapsed) {
          max-height: none !important;
          overflow-y: scroll !important;
          overflow-anchor: none !important;
        }
      `;
      document.head.appendChild(style);
    })();
    let lastExpandedItem = null;
    // 根据 SettingPanel.state 读取是否开启“全部展开”模式;用根节点 class 表达全局展开,避免逐串同步改 class 堵塞版块页
    const expandAllMode = !!(typeof SettingPanel !== 'undefined' && SettingPanel.state && SettingPanel.state.enablePostExpandAll);
    document.documentElement.classList.toggle('xdex-post-expand-all', expandAllMode);
    // 外部点击收起:兼容两种模式(常规 / 全部展开)
    function outsideHandler(e) {
      // 忽略公用折叠占位符点击
      if (e.target.closest('.xdex-placeholder.xdex-generic-toggle')) return;
      const cfg = (typeof SettingPanel !== 'undefined' && SettingPanel.state) ? SettingPanel.state : {};
      const isAllMode = !!cfg.enablePostExpandAll;
      if (!isAllMode) {
        // 必须在 #h-content 内
        const hContent = document.getElementById('h-content');
      if (!hContent || !hContent.contains(e.target)) return;
      // 找到所有 .h-threads-list,判断点击是否在任一 list 的垂直范围内
      const threadLists = document.querySelectorAll('#h-content .h-threads-list');
      if (!threadLists.length) return;
      const x = e.clientX;
      const y = e.clientY;
      let matchedListRect = null;
      let isInAnyList = false;
      for (const list of threadLists) {
        const listRect = list.getBoundingClientRect();
        if (y >= listRect.top && y <= listRect.bottom) {
          isInAnyList = true;
          matchedListRect = listRect;
          break;
        }
      }
      if (!isInAnyList) return;
      if (!matchedListRect) return;
      if (y < matchedListRect.top || y > matchedListRect.bottom) return;
        // 点击在 uk-container 内部不处理(排除串内部及其包裹容器)
        if (e.target.closest('.uk-container')) return;
        // 如果没有记录到最后被展开的串,则不处理
        if (!lastExpandedItem) return;
        // 确保 lastExpandedItem 仍在 DOM 中
        if (!document.contains(lastExpandedItem)) {
          lastExpandedItem = null;
          return;
        }
        // 获取被展开串的 rect
        const rect = lastExpandedItem.getBoundingClientRect();
        // 若点击纵坐标不在该串的垂直范围内,则不收起
        const V_TOL = 2; // 垂直容差(像素)
        if (y < rect.top - V_TOL || y > rect.bottom + V_TOL) return;
        // 若点击点在该串的水平内部范围内,不收起;只在左右明确外围才收起
        if (x >= rect.left && x <= rect.right) return;
        // 排除点击在上方兄弟元素内
        let p = lastExpandedItem.parentElement;
        while (p && p !== document.body) {
          for (let sib = p.firstElementChild; sib && sib !== lastExpandedItem; sib = sib.nextElementSibling) {
            if (sib.contains(e.target)) {
              return;
            }
          }
          if (p.classList && p.classList.contains('h-threads-list')) break;
          p = p.parentElement;
        }
        // 触发收起
        const btn = lastExpandedItem.querySelector('.h-threads-info .js-toggle-mode');
        if (btn) setPostExpandToggleVisual(btn, false);
        collapseWithoutShift(lastExpandedItem);
        lastExpandedItem = null;
        return;
      }
      // ===== 全部展开模式保留原逻辑,但同样采用“纵向带 + 排除 uk-container” =====
      const hContent = document.getElementById('h-content');
      if (!hContent || !hContent.contains(e.target)) return;
      const threadLists = document.querySelectorAll('#h-content .h-threads-list');
      if (!threadLists.length) return;
      const y = e.clientY;
      let isInAnyList = false;
      for (const list of threadLists) {
        const listRect = list.getBoundingClientRect();
        if (y >= listRect.top && y <= listRect.bottom) {
          isInAnyList = true;
          break;
        }
      }
      if (!isInAnyList) return;
      // 点击在 uk-container 内部不处理
      if (e.target.closest('.uk-container')) return;
      const threads = Array.from(document.querySelectorAll('.h-threads-item-index'));
      if (!threads.length) return;
      let target = threads.find(t => {
        const r = t.getBoundingClientRect();
        return y >= r.top && y <= r.bottom;
      });
      if (!target) {
        let min = Infinity;
        for (const t of threads) {
          const r = t.getBoundingClientRect();
          const d = Math.min(Math.abs(y - r.top), Math.abs(y - r.bottom));
          if (d < min) { min = d; target = t; }
        }
      }
      if (target && !target.classList.contains('xdex-post-expand-collapsed')) {
        const btn = target.querySelector('.h-threads-info .js-toggle-mode');
        if (btn) setPostExpandToggleVisual(btn, false);
        target.classList.add('xdex-post-expand-collapsed');
        collapseWithoutShift(target);
      }
    }
    // 添加按钮
    scanTargets.forEach(item => {
      // === 新增:跳过主串自身被折叠的情况(不包括串内回复被折叠) ===
      // 只检查 item 的直接子元素中是否有折叠占位符,不深入检查回复区域
      const directPlaceholder = Array.from(item.children).find(child =>
        child.classList.contains('xdex-placeholder') &&
        child.classList.contains('xdex-generic-toggle')
      );
      if (directPlaceholder) return;
      const infoBar = item.querySelector('.h-threads-info');
      if (!infoBar) return;
      if (infoBar.querySelector('.js-toggle-mode')) return;
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'js-toggle-mode xdex-icon-btn';
      btn.style.cssText = 'min-width:1em;text-align:center;vertical-align:middle;margin-left:4px;';
      setPostExpandToggleVisual(btn, expandAllMode && !item.classList.contains('xdex-post-expand-collapsed'));
      btn.addEventListener('click', e => {
        e.stopPropagation();
        const isAllMode = document.documentElement.classList.contains('xdex-post-expand-all');
        if (isAllMode) {
          const willExpand = item.classList.contains('xdex-post-expand-collapsed');
          if (willExpand) {
            item.classList.remove('xdex-post-expand-collapsed');
            setPostExpandToggleVisual(btn, true);
            lastExpandedItem = item;
          } else {
            setPostExpandToggleVisual(btn, false);
            item.classList.add('xdex-post-expand-collapsed');
            collapseWithoutShift(item);
            if (lastExpandedItem === item) lastExpandedItem = null;
          }
          return;
        }
        const willExpand = !item.classList.contains('expanded');
        if (willExpand) {
          item.classList.add('expanded');
          setPostExpandToggleVisual(btn, true);
          lastExpandedItem = item;
        } else {
          setPostExpandToggleVisual(btn, false);
          collapseWithoutShift(item);
          if (lastExpandedItem === item) lastExpandedItem = null;
        }
      });
      infoBar.appendChild(btn);
    });
    // 绑定外部点击
    const hContent = document.getElementById('h-content');
    if (hContent) {
      hContent.removeEventListener('click', outsideHandler, true);
      hContent.addEventListener('click', outsideHandler, true);
    }
  }

  /* --------------------------------------------------
   * tag 17. 替换搜索按钮为野生搜索酱(来自4sYbzEX https://www.nmbxd.com/t/64792841)
   * -------------------------------------------------- */
  function searchServiceBy4sY() {
    const btn = document.querySelector('#h-menu-search-keyword');
    if (!btn) return; // 若按钮不存在则不处理
    // 移除原有的 href 与默认跳转行为
    btn.removeAttribute('href');
    btn.removeEventListener('click', btn._4sYHandler || (()=>{}));
    // 定义新的点击事件
    const handler = (e) => {
      e.preventDefault();
      window.open('https://nmb-search.166666666.xyz/', '_blank');
    };
    // 绑定事件
    btn.addEventListener('click', handler);
    // 保存 handler 以便后续移除时使用(防止重复绑定)
    btn._4sYHandler = handler;
  }
  function isNmbSearchPage() {
    return location.hostname === 'nmb-search.166666666.xyz';
  }
  function normalizeNmbSearchMobileThreadUrl(rawHref) {
    let url;
    try {
      url = new URL(rawHref, location.href);
    } catch (e) {
      return rawHref;
    }
    if (url.protocol !== 'https:' || url.hostname !== 'www.nmbxd1.com') return rawHref;
    const nextPath = url.pathname.replace(/^\/m\/t\/(\d+)/, '/t/$1');
    if (nextPath === url.pathname) return rawHref;
    url.pathname = nextPath;
    return url.toString();
  }
  function makeNmbSearchLinkOpenInNewTab(a) {
    a.setAttribute('target', '_blank');
    const rel = new Set(String(a.getAttribute('rel') || '').split(/\s+/).filter(Boolean));
    rel.add('noopener');
    rel.add('noreferrer');
    a.setAttribute('rel', Array.from(rel).join(' '));
  }
  function isNmbSearchResultLink(a) {
    return !!(a && a.matches && a.matches('#overflow a[href]'));
  }
  function rewriteNmbSearchMobileThreadLinks(root = document) {
    if (!isNmbSearchPage()) return;
    const selector = '#overflow a[href]';
    const links = [];
    if (root && root.matches && root.matches(selector)) links.push(root);
    if (root && root.querySelectorAll) {
      root.querySelectorAll(selector).forEach(a => links.push(a));
    }
    links.forEach(a => {
      const rawHref = a.getAttribute('href') || a.href || '';
      const nextHref = normalizeNmbSearchMobileThreadUrl(rawHref);
      if (nextHref !== rawHref) a.setAttribute('href', nextHref);
      makeNmbSearchLinkOpenInNewTab(a);
    });
  }
  function handleNmbSearchLinkClick(e) {
    if (!isNmbSearchPage() || e.defaultPrevented || e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
    const a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
    if (!a) return;
    if (!isNmbSearchResultLink(a)) return;
    const rawHref = a.getAttribute('href') || a.href || '';
    if (!rawHref || /^javascript:/i.test(rawHref)) return;
    const nextHref = normalizeNmbSearchMobileThreadUrl(rawHref);
    if (nextHref !== rawHref) a.setAttribute('href', nextHref);
    makeNmbSearchLinkOpenInNewTab(a);
    e.preventDefault();
    if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
    window.open(a.href, '_blank', 'noopener,noreferrer');
  }
  function initNmbSearchMobileThreadRedirector() {
    if (!isNmbSearchPage()) return;
    rewriteNmbSearchMobileThreadLinks(document);
    if (!initNmbSearchMobileThreadRedirector.clickHandlerInstalled) {
      document.addEventListener('click', handleNmbSearchLinkClick, true);
      initNmbSearchMobileThreadRedirector.clickHandlerInstalled = true;
    }
    if (initNmbSearchMobileThreadRedirector.observer || !document.body) return;
    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
          if (node && node.nodeType === 1) rewriteNmbSearchMobileThreadLinks(node);
        });
      });
    });
    observer.observe(document.body, { childList: true, subtree: true });
    initNmbSearchMobileThreadRedirector.observer = observer;
  }

  /* --------------------------------------------------
   * tag 18. 侧栏收起(来自acVMxuvhttps://greasyfork.org/zh-CN/scripts/553143-x%E5%B2%9B%E4%BC%98%E5%8C%96%E5%B2%9B-%E4%BE%A7%E8%BE%B9%E6%A0%8F%E4%BC%98%E5%8C%96%E7%89%88)
   * -------------------------------------------------- */
  function toggleSidebar() {
    // --- 侧边栏自动收起功能 ---
    const hMenuDiv = document.getElementById('h-menu');
    if (hMenuDiv) {
        // 1. 创建一个新的触发区域元素
        const triggerZone = document.createElement('div');
        // 2. 设置触发区域的样式
        // 它将是一个位于屏幕左侧的、透明的、固定宽度的垂直条
        triggerZone.style.position = 'fixed';
        triggerZone.style.left = '0';
        triggerZone.style.top = '0';
        triggerZone.style.width = '100px'; // 触发区域宽度,可以根据需要调整
        triggerZone.style.height = '100vh'; // 高度占满整个屏幕
        triggerZone.style.zIndex = '800'; // 确保它在大多数元素之上,但在菜单之下
        // triggerZone.style.backgroundColor = 'rgba(255, 0, 0, 0.2)'; // 取消此行注释以可视化触发区域
        // 3. 将触发区域添加到页面中
        document.body.appendChild(triggerZone);
        // 4. 设置侧边栏的初始样式为折叠
        hMenuDiv.style.transition = 'width 0.3s ease'; // 添加平滑过渡效果
        hMenuDiv.style.width = '0';
        hMenuDiv.style.overflow = 'hidden';
        hMenuDiv.style.zIndex = '999'; // 确保菜单在触发区域之上
        // 5. 为新的触发区域添加鼠标悬停事件,用于展开侧边栏
        triggerZone.addEventListener('mouseover', function() {
            hMenuDiv.style.width = '200px'; // 展开侧边栏
            hMenuDiv.style.overflow = 'visible';
        });
        // 6. 为侧边栏本身添加鼠标移出事件,用于收起侧边栏
        hMenuDiv.addEventListener('mouseleave', function() {
            hMenuDiv.style.width = '0'; // 收起侧边栏
            hMenuDiv.style.overflow = 'hidden';
        });
    }
  }

  /* --------------------------------------------------
   * tag 19. 替换顶栏图片点击事件,串内刷新,板块页回到首页
   * -------------------------------------------------- */
  function overrideTopImageClick() {
    const topImgLink = document.querySelector('#h-menu-top-img');
    if (!topImgLink) return false;
    // 幂等:early / ready 可能各跑一次
    if (topImgLink.dataset.xdexTopImgClickBound === '1') return true;
    topImgLink.dataset.xdexTopImgClickBound = '1';
    // 判断是否是串内页
    function isThreadPage(path) {
        return /\/t\/\d{4,}/.test(path) || /^\/Forum\/po\/id\/\d+/.test(path);
    }
    // 判断是否是板块页
    function isBoardPage(path) {
        return /^\/f\//.test(path) || /^\/Forum\/timeline\/id\/\d+/.test(path);
    }
    // capture 阶段抢先,避免原站默认跳转先触发
    topImgLink.addEventListener('click', function(e) {
        e.preventDefault();
        e.stopPropagation();
        if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
        const path = location.pathname;
        let url = location.href;
        if (isThreadPage(path)) {
            // 串内页:刷新当前页面
            location.reload();
        } else if (isBoardPage(path)) {
            // 板块页:跳转到第一页
            if (/\/Forum\/timeline\/id\/\d+\/page\/\d+\.html/.test(url)) {
                url = url.replace(/\/page\/\d+\.html/, '/page/1.html');
            } else if (/[?&]page=\d+/.test(url)) {
                url = url.replace(/([?&])page=\d+/, '$1page=1');
            } else if (/\/Forum\/timeline\/id\/\d+/.test(path) || /^\/f\//.test(path)) {
                // 已是第一页或无 page 参数:仍强制回第一页语义
                try {
                  const u = new URL(url, location.origin);
                  u.searchParams.set('page', '1');
                  url = u.toString();
                } catch (_) {}
            }
            location.href = url;
        } else {
            // 其他情况:跳转首页
            location.href = '/';
        }
    }, true);
    return true;
  }
  // early 安装:顶栏节点一出现就绑;若尚未出现则短时观察,避免等 document.ready/batch2
  function installOverrideTopImageClickEarly() {
    if (installOverrideTopImageClickEarly.__done) return;
    if (overrideTopImageClick()) {
      installOverrideTopImageClickEarly.__done = true;
      return;
    }
    if (installOverrideTopImageClickEarly.__observing) return;
    const root = document.documentElement || document;
    if (!root || typeof MutationObserver === 'undefined') return;
    installOverrideTopImageClickEarly.__observing = true;
    const obs = new MutationObserver(() => {
      if (overrideTopImageClick()) {
        installOverrideTopImageClickEarly.__done = true;
        installOverrideTopImageClickEarly.__observing = false;
        try { obs.disconnect(); } catch (_) {}
      }
    });
    try {
      obs.observe(root, { childList: true, subtree: true });
    } catch (_) {
      installOverrideTopImageClickEarly.__observing = false;
    }
    // 兜底:最长观察 8s,避免永久挂着 observer
    setTimeout(() => {
      if (installOverrideTopImageClickEarly.__done) return;
      try { obs.disconnect(); } catch (_) {}
      installOverrideTopImageClickEarly.__observing = false;
      overrideTopImageClick();
    }, 8000);

  }
  /* --------------------------------------------------
   * tag 20. 图片显示模式:默认/模糊/无图/Tips
   * -------------------------------------------------- */
  function applyImageHideMode(mode = 'default', root = document) {
    const isImageHideExcludedPage = location.hostname === 'www.nmbxd1.com'
      && (location.pathname.startsWith('/Member/') || location.pathname.startsWith('/Forum'));
    if (isImageHideExcludedPage) return;
    return startupPerfDebug.measure('applyImageHideMode', () => {
    const COVER_URL = 'https://moetu.org/xdchan/cover.php?from=index';
    const VALID_MODES = new Set(['default', 'blur', 'noimage', 'tips', 'none']);
    const finalMode = VALID_MODES.has(mode) ? mode : 'default';
    const scope = root && root.querySelectorAll ? root : document;
    const coverCache = applyImageHideMode.__coverCache || (applyImageHideMode.__coverCache = []);
    const coverCacheSet = applyImageHideMode.__coverCacheSet || (applyImageHideMode.__coverCacheSet = new Set());
    const COVER_CACHE_MAX = 80;
    // 样式只注入一次
    if (!document.getElementById('xdex-image-hide-style')) {
      const style = document.createElement('style');
      style.id = 'xdex-image-hide-style';
      style.textContent = `
        /* 模式1:模糊遮罩
           clip-path: inset(0) 裁剪 filter 产生的合成层溢出(overflow:hidden 做不到) */
        .h-threads-img-box.xdex-hide-blur:not(.h-active) .h-threads-img {
          filter: blur(14px) brightness(0.5);
          clip-path: inset(0);
          transition: filter .15s ease;
        }
        /* 悬浮时取消遮罩 */
        .h-threads-img-box.xdex-hide-blur:not(.h-active):hover .h-threads-img {
          filter: none;
          clip-path: none;
        }
        /* 放大(h-active)时始终不遮罩 */
        .h-threads-img-box.xdex-hide-blur.h-active .h-threads-img {
          filter: none !important;
          clip-path: none !important;
        }
        /* 独立 img(无容器时) */
        img.xdex-hide-blur-img {
          filter: blur(14px) brightness(0.5);
          clip-path: inset(0);
          transition: filter .15s ease;
        }
        img.xdex-hide-blur-img:hover {
          filter: none;
          clip-path: none;
        }
        /* 模式2:无图(隐藏,不删除 DOM) */
        .h-threads-img-box.xdex-hide-noimage {
          display: none !important;
        }
        img.xdex-hide-noimage-img {
          display: none !important;
        }
      `;
      document.head.appendChild(style);
    }
    function getAllImgs(target) {
      const imgs = [];
      if (target instanceof HTMLImageElement) imgs.push(target);
      if (target && target.querySelectorAll) {
        target.querySelectorAll('img').forEach(img => imgs.push(img));
      }
      return imgs;
    }
    function addCoverToCache(url) {
      if (!url || coverCacheSet.has(url)) return;
      coverCacheSet.add(url);
      coverCache.push(url);
      if (coverCache.length > COVER_CACHE_MAX) {
        const removed = coverCache.shift();
        if (removed) coverCacheSet.delete(removed);
      }
    }
    function removeCoverFromCache(url) {
      if (!url || !coverCacheSet.has(url)) return;
      coverCacheSet.delete(url);
      const idx = coverCache.indexOf(url);
      if (idx >= 0) coverCache.splice(idx, 1);
    }
    function buildNewCoverUrl(idx) {
      return `${COVER_URL}&xdex=${Date.now()}_${idx}_${Math.random().toString(36).slice(2)}`;
    }
    function pickCoverUrl(idx, triedSet) {
      // 优先复用缓存 URL,降低重复请求压力
      const usableCached = coverCache.filter(u => !triedSet.has(u));
      if (usableCached.length > 0) {
        const u = usableCached[Math.floor(Math.random() * usableCached.length)];
        return { url: u, fromCache: true };
      }
      return { url: buildNewCoverUrl(idx), fromCache: false };
    }
    function isExcludedImage(img) {
      if (!img || !img.closest) return false;
      try {
        return !!img.closest([
          '#h-bottom-nav',
          '#h-menu-top',
          '#h-preview-box',
          '.h-preview-box',
          '#sp_btn',
          '.xdex-image-viewer-btn',
          '.h-threads-item-reply[data-threads-id="9999999"]'
        ].join(', '));
      } catch (e) {
        // 选择器异常时兜底,避免中断后续逻辑
        return false;
      }
    }
    function restoreNoImage(img) {
      if (!img || img.dataset.xdexNoImgApplied !== '1') return;
      const box = img.closest('.h-threads-img-box');
      if (box) {
        box.classList.remove('xdex-hide-noimage');
        box.dataset.xdexNoImgBox = '0';
      }
      img.classList.remove('xdex-hide-noimage-img');
      img.dataset.xdexNoImgImg = '0';
      delete img.dataset.xdexNoImgApplied;
    }
    function restoreReplacedImage(img) {
      if (!img || img.dataset.xdexHideReplaceApplied !== '1') return;
      const origSrc = img.dataset.xdexOrigSrc;
      const origSrcset = img.dataset.xdexOrigSrcset;
      const origSizes = img.dataset.xdexOrigSizes;
      const origAlt = img.dataset.xdexOrigAlt;
      const origTitle = img.dataset.xdexOrigTitle;
      const origStyle = img.dataset.xdexOrigStyle;
      if (origSrc) img.setAttribute('src', origSrc); else img.removeAttribute('src');
      if (origSrcset) img.setAttribute('srcset', origSrcset); else img.removeAttribute('srcset');
      if (origSizes) img.setAttribute('sizes', origSizes); else img.removeAttribute('sizes');
      if (origAlt) img.setAttribute('alt', origAlt); else img.removeAttribute('alt');
      if (origTitle) img.setAttribute('title', origTitle); else img.removeAttribute('title');
      if (origStyle) img.setAttribute('style', origStyle); else img.removeAttribute('style');
      const box = img.closest('.h-threads-img-box');
      if (box) box.dataset.xdexHideReplaceBox = '0';
      if (img.__xdexRestoreOnClick) {
        img.removeEventListener('click', img.__xdexRestoreOnClick, true);
        img.__xdexRestoreOnClick = null;
      }
      img.onload = null;
      img.onerror = null;
      delete img.dataset.xdexHideReplaceApplied;
      // 恢复懒加载属性
      if (img.dataset.xdexOrigHdSrc) {
        img.dataset.xdexHdSrc = img.dataset.xdexOrigHdSrc;
        delete img.dataset.xdexOrigHdSrc;
      }
      if (img.dataset.xdexOrigThumbSrc) {
        img.dataset.xdexThumbSrc = img.dataset.xdexOrigThumbSrc;
        delete img.dataset.xdexOrigThumbSrc;
      }
      delete img.dataset.xdexOrigSrc;
      delete img.dataset.xdexOrigSrcset;
      delete img.dataset.xdexOrigSizes;
      delete img.dataset.xdexOrigAlt;
      delete img.dataset.xdexOrigTitle;
      delete img.dataset.xdexOrigStyle;
      delete img.dataset.xdexLockWidth;
      delete img.dataset.xdexLockHeight;
    }
    function clearBlurMarks(target) {
      if (!target || !target.querySelectorAll) return;
      target.querySelectorAll('.h-threads-img-box.xdex-hide-blur').forEach(box => {
        box.classList.remove('xdex-hide-blur');
        box.dataset.xdexHideBlurBox = '0';
      });
      target.querySelectorAll('img.xdex-hide-blur-img').forEach(img => {
        img.classList.remove('xdex-hide-blur-img');
        img.dataset.xdexHideBlurImg = '0';
      });
    }
    function clearNoImageMarks(target) {
      if (!target || !target.querySelectorAll) return;
      target.querySelectorAll('.h-threads-img-box.xdex-hide-noimage').forEach(box => {
        box.classList.remove('xdex-hide-noimage');
        box.dataset.xdexNoImgBox = '0';
      });
      target.querySelectorAll('img.xdex-hide-noimage-img').forEach(img => {
        img.classList.remove('xdex-hide-noimage-img');
        img.dataset.xdexNoImgImg = '0';
      });
      target.querySelectorAll('img[data-xdex-no-img-applied="1"]').forEach(restoreNoImage);
    }
    function applyBlur(img) {
      const box = img.closest('.h-threads-img-box');
      // blur 模式下若之前替换过,先恢复原图
      if (img.dataset.xdexHideReplaceApplied === '1') {
        restoreReplacedImage(img);
      }
      // blur 模式下若之前处于无图,先恢复
      if (img.dataset.xdexNoImgApplied === '1') {
        restoreNoImage(img);
      }
      if (box) {
        if (box.dataset.xdexHideBlurBox === '1') return;
        box.classList.add('xdex-hide-blur');
        box.dataset.xdexHideBlurBox = '1';
        return;
      }
      if (img.dataset.xdexHideBlurImg === '1') return;
      img.classList.add('xdex-hide-blur-img');
      img.dataset.xdexHideBlurImg = '1';
    }
    function applyNoImage(img) {
      const box = img.closest('.h-threads-img-box');
      if (img.dataset.xdexHideReplaceApplied === '1') {
        restoreReplacedImage(img);
      }
      if (box) {
        box.classList.remove('xdex-hide-blur');
        box.dataset.xdexHideBlurBox = '0';
      }
      img.classList.remove('xdex-hide-blur-img');
      img.dataset.xdexHideBlurImg = '0';
      if (img.dataset.xdexNoImgApplied === '1') return;
      img.dataset.xdexNoImgApplied = '1';
      if (box) {
        box.classList.add('xdex-hide-noimage');
        box.dataset.xdexNoImgBox = '1';
      } else {
        img.classList.add('xdex-hide-noimage-img');
        img.dataset.xdexNoImgImg = '1';
      }
    }
    function applyReplace(img, idx) {
      const box = img.closest('.h-threads-img-box');
      // Tips 模式下若之前处于无图,先恢复
      if (img.dataset.xdexNoImgApplied === '1') {
        restoreNoImage(img);
      }
      // Tips 模式下移除 blur 标记
      if (box) {
        box.classList.remove('xdex-hide-blur');
        box.dataset.xdexHideBlurBox = '0';
        box.classList.remove('xdex-hide-noimage');
        box.dataset.xdexNoImgBox = '0';
      }
      img.classList.remove('xdex-hide-blur-img');
      img.dataset.xdexHideBlurImg = '0';
      img.classList.remove('xdex-hide-noimage-img');
      img.dataset.xdexNoImgImg = '0';
      if (img.dataset.xdexHideReplaceApplied === '1') return;
      // 锁定当前显示尺寸,防止替换图挤压回复区域
      const rect = img.getBoundingClientRect();
      const cs = window.getComputedStyle(img);
      const lockW = Math.round(rect.width || parseFloat(cs.width) || 0);
      const lockH = Math.round(rect.height || parseFloat(cs.height) || 0);
      img.dataset.xdexOrigSrc = img.getAttribute('src') || '';
      img.dataset.xdexOrigSrcset = img.getAttribute('srcset') || '';
      img.dataset.xdexOrigSizes = img.getAttribute('sizes') || '';
      img.dataset.xdexOrigAlt = img.getAttribute('alt') || '';
      img.dataset.xdexOrigTitle = img.getAttribute('title') || '';
      img.dataset.xdexOrigStyle = img.getAttribute('style') || '';
      img.dataset.xdexLockWidth = lockW > 0 ? String(lockW) : '';
      img.dataset.xdexLockHeight = lockH > 0 ? String(lockH) : '';
      img.dataset.xdexHideReplaceApplied = '1';
      // 暂存并清除懒加载属性,防止懒加载器覆盖 tips 图
      if (img.dataset.xdexHdSrc) {
        img.dataset.xdexOrigHdSrc = img.dataset.xdexHdSrc;
        delete img.dataset.xdexHdSrc;
      }
      if (img.dataset.xdexThumbSrc) {
        img.dataset.xdexOrigThumbSrc = img.dataset.xdexThumbSrc;
      }
      delete img.dataset.xdexHdLoaded;
      delete img.dataset.xdexHdLoading;
      if (box) box.dataset.xdexHideReplaceBox = '1';
      // 不预加载原图:仅保存原链接,点击恢复时再加载
      img.removeAttribute('srcset');
      img.removeAttribute('sizes');
      img.setAttribute('title', 'Tips');
      img.setAttribute('alt', 'Tips');
      if (lockW > 0) {
        img.style.width = `${lockW}px`;
        img.style.minWidth = `${lockW}px`;
        img.style.maxWidth = `${lockW}px`;
      } else {
        img.style.maxWidth = '100%';
      }
      if (lockH > 0) {
        img.style.height = `${lockH}px`;
      } else {
        img.style.height = 'auto';
      }
      img.style.maxHeight = '60vh';
      img.style.objectFit = 'cover';
      const tried = new Set();
      const applyCoverWithRetry = (remain = 6) => {
        if (img.dataset.xdexHideReplaceApplied !== '1') return;
        const picked = pickCoverUrl(idx, tried);
        const fakeUrl = picked.url;
        tried.add(fakeUrl);
        img.onload = () => {
          addCoverToCache(fakeUrl);
          img.onload = null;
          img.onerror = null;
        };
        img.onerror = () => {
          removeCoverFromCache(fakeUrl);
          if (remain <= 0) {
            img.onload = null;
            img.onerror = null;
            return;
          }
          // 失败后立即尝试下一张(优先缓存池,其次新 URL)
          applyCoverWithRetry(remain - 1);
        };
        img.setAttribute('src', fakeUrl);
      };
      applyCoverWithRetry();
      // 首次点击:仅恢复原图;后续点击回归原逻辑
      const restoreOnFirstClick = (e) => {
        if (img.dataset.xdexHideReplaceApplied !== '1') return;
        e.preventDefault();
        e.stopPropagation();
        if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
        restoreReplacedImage(img);
      };
      img.__xdexRestoreOnClick = restoreOnFirstClick;
      img.addEventListener('click', restoreOnFirstClick, true);
    }
    if (finalMode === 'none' || finalMode === 'default') {
      clearBlurMarks(document);
      clearNoImageMarks(document);
      document.querySelectorAll('img[data-xdex-hide-replace-applied="1"]').forEach(restoreReplacedImage);
      window.__xdexImageHideMode = 'default';
      try {
        if (typeof syncImageViewerButtonForHideMode === 'function') {
          syncImageViewerButtonForHideMode('default');
        }
      } catch (e) {}
      return;
    }
    const imgs = getAllImgs(scope);
    imgs.forEach((img, idx) => {
      // 排除顶栏和底栏导航中的图片,不参与替换/模糊
      if (isExcludedImage(img)) {
        if (img.dataset.xdexHideReplaceApplied === '1') restoreReplacedImage(img);
        if (img.dataset.xdexNoImgApplied === '1') restoreNoImage(img);
        img.classList.remove('xdex-hide-blur-img');
        img.dataset.xdexHideBlurImg = '0';
        img.classList.remove('xdex-hide-noimage-img');
        img.dataset.xdexNoImgImg = '0';
        const box = img.closest('.h-threads-img-box');
        if (box) {
          box.classList.remove('xdex-hide-blur');
          box.classList.remove('xdex-hide-noimage');
          box.dataset.xdexHideBlurBox = '0';
          box.dataset.xdexNoImgBox = '0';
          box.dataset.xdexHideReplaceBox = '0';
        }
        return;
      }
      // 跳过无 src 的占位图(但保留 h-threads-img 处理)
      if (!img.classList.contains('h-threads-img') && !(img.getAttribute('src') || '').trim()) return;
      if (finalMode === 'blur') applyBlur(img);
      else if (finalMode === 'noimage') applyNoImage(img);
      else if (finalMode === 'tips') applyReplace(img, idx);
    });
    window.__xdexImageHideMode = finalMode;
    // 阅图入口与隐藏模式联动(无图隐藏可恢复 / 模糊遮罩 / Tips 换皮)
    try {
      if (typeof syncImageViewerButtonForHideMode === 'function') {
        syncImageViewerButtonForHideMode(finalMode);
      }
    } catch (e) {}
    }, () => Object.assign({ mode }, startupPerfDebug.summarizeRoot(root || document)));
  }
  // 暴露给外部:window.applyImageHideMode('default'|'blur'|'noimage'|'tips'|'none', root)
  window.applyImageHideMode = applyImageHideMode;

  /* --------------------------------------------------
   * tag 21. 自动识别链接
   * -------------------------------------------------- */
  function runAutoUrlLinkify(root = document) {
    return startupPerfDebug.measure('runAutoUrlLinkify', () => {
    const scope = root && root.querySelectorAll ? root : document;
    const processedAttr = 'data-xdex-linkified';
    const containerSelector = '.h-threads-content, .h-preview-box';
    const protocolUrlRegex = /https?:\/\/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d{2,5})?(?:\/[^\s<,。!?;:、】【)》」』、?#]*)*(?:\?[^\s<,。!?;:、】【)》」』、#]*)?(?:#[^\s<,。!?;:、】【)》」』、]*)?/gi;
    const wwwUrlRegex = /www\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d{2,5})?(?:\/[^\s<,。!?;:、】【)》」』、?#]*)*(?:\?[^\s<,。!?;:、】【)》」』、#]*)?(?:#[^\s<,。!?;:、】【)》」』、]*)?/gi;
    const bareDomainScanRegex = /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d{2,5})?(?:\/[^\s<,。!?;:、】【)》」』、?#]*)*(?:\?[^\s<,。!?;:、】【)》」』、#]*)?(?:#[^\s<,。!?;:、】【)》」』、]*)?/gi;
    const bareDomainCandidateRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d{2,5})?(?:\/[^\s<,。!?;:、】【)》」』、?#]*)*(?:\?[^\s<,。!?;:、】【)》」』、#]*)?(?:#[^\s<,。!?;:、】【)》」』、]*)?$/i;
    const trailingPunctuationRegex = /[),.!?\]}'",。!?;:、】【)》」』、]+$/;
    const skipTags = new Set(['A', 'CODE', 'PRE', 'SCRIPT', 'STYLE', 'TEXTAREA', 'INPUT', 'SELECT', 'BUTTON']);
    const shouldSkipTextNode = (node) => {
      const parent = node.parentElement;
      if (!parent) return true;
      if (parent.closest('a, code, pre, script, style, textarea, input, select, button')) return true;
      return skipTags.has(parent.tagName);
    };
    const normalizeHref = (value) => {
      if (/^https?:\/\//i.test(value)) return value;
      return `https://${value}`;
    };
    const hasTightBoundary = (char) => !!char && /[0-9A-Za-z]/.test(char);
    const extractCandidate = (text, match, type) => {
      let matchedText = match[0];
      while (trailingPunctuationRegex.test(matchedText)) {
        matchedText = matchedText.replace(trailingPunctuationRegex, '');
      }
      if (!matchedText) return null;
      const startIndex = match.index;
      const endIndex = startIndex + matchedText.length;
      const prevChar = text[startIndex - 1] || '';
      const nextChar = text[endIndex] || '';
      const isBareDomain = type === 'bare';
      if (type === 'www' && hasTightBoundary(prevChar)) {
        return null;
      }
      if (type === 'bare' && hasTightBoundary(prevChar)) {
        return null;
      }
      if (hasTightBoundary(nextChar)) {
        return null;
      }
      if (isBareDomain) {
        if (!bareDomainCandidateRegex.test(matchedText)) return null;
      }
      return {
        text: matchedText,
        href: normalizeHref(matchedText),
        startIndex,
        endIndex
      };
    };
    const linkifyTextNode = (textNode) => {
      const text = textNode.nodeValue || '';
      if (!text.trim()) return;
      protocolUrlRegex.lastIndex = 0;
      wwwUrlRegex.lastIndex = 0;
      bareDomainScanRegex.lastIndex = 0;
      const matches = [
        ...Array.from(text.matchAll(protocolUrlRegex)).map(match => ({ match, type: 'protocol' })),
        ...Array.from(text.matchAll(wwwUrlRegex)).map(match => ({ match, type: 'www' })),
        ...Array.from(text.matchAll(bareDomainScanRegex)).map(match => ({ match, type: 'bare' }))
      ].sort((a, b) => a.match.index - b.match.index || b.match[0].length - a.match[0].length);
      if (matches.length === 0) return;
      const fragment = document.createDocumentFragment();
      let cursor = 0;
      let changed = false;
      matches.forEach(({ match, type }) => {
        const candidate = extractCandidate(text, match, type);
        if (!candidate) return;
        if (candidate.startIndex < cursor) return;
        if (candidate.startIndex > cursor) {
          fragment.appendChild(document.createTextNode(text.slice(cursor, candidate.startIndex)));
        }
        const anchor = document.createElement('a');
        anchor.href = candidate.href;
        anchor.target = '_blank';
        anchor.rel = 'noopener noreferrer';
        anchor.textContent = candidate.text;
        anchor.style.color = '#007bff';
        anchor.style.textDecoration = 'none';
        anchor.addEventListener('mouseenter', () => {
          anchor.style.textDecoration = 'underline';
          anchor.style.textDecorationColor = '#007bff';
        });
        anchor.addEventListener('mouseleave', () => {
          anchor.style.textDecoration = 'none';
        });
        fragment.appendChild(anchor);
        cursor = candidate.endIndex;
        changed = true;
      });
      if (!changed) return;
      if (cursor < text.length) {
        fragment.appendChild(document.createTextNode(text.slice(cursor)));
      }
      textNode.parentNode.replaceChild(fragment, textNode);
    };
    const processContainer = (container) => {
      if (!container) return;
      const pendingWalker = document.createTreeWalker(
        container,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode(node) {
            if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
            if (shouldSkipTextNode(node)) return NodeFilter.FILTER_REJECT;
            protocolUrlRegex.lastIndex = 0;
            if (protocolUrlRegex.test(node.nodeValue)) return NodeFilter.FILTER_ACCEPT;
            wwwUrlRegex.lastIndex = 0;
            if (wwwUrlRegex.test(node.nodeValue)) return NodeFilter.FILTER_ACCEPT;
            const bareMatches = Array.from(node.nodeValue.matchAll(bareDomainScanRegex));
            return bareMatches.some(({ 0: value, index }) => extractCandidate(node.nodeValue, { 0: value, index }, 'bare'))
              ? NodeFilter.FILTER_ACCEPT
              : NodeFilter.FILTER_REJECT;
          }
        }
      );
      const hasPendingText = !!pendingWalker.nextNode();
      if (container.getAttribute(processedAttr) === '1' && !hasPendingText) return;
      const walker = document.createTreeWalker(
        container,
        NodeFilter.SHOW_TEXT,
        {
          acceptNode(node) {
            if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
            if (shouldSkipTextNode(node)) return NodeFilter.FILTER_REJECT;
            protocolUrlRegex.lastIndex = 0;
            if (protocolUrlRegex.test(node.nodeValue)) return NodeFilter.FILTER_ACCEPT;
            wwwUrlRegex.lastIndex = 0;
            if (wwwUrlRegex.test(node.nodeValue)) return NodeFilter.FILTER_ACCEPT;
            const bareMatches = Array.from(node.nodeValue.matchAll(bareDomainScanRegex));
            return bareMatches.some(({ 0: value, index }) => extractCandidate(node.nodeValue, { 0: value, index }, 'bare'))
              ? NodeFilter.FILTER_ACCEPT
              : NodeFilter.FILTER_REJECT;
          }
        }
      );
      const nodes = [];
      let currentNode;
      while ((currentNode = walker.nextNode())) {
        nodes.push(currentNode);
      }
      nodes.forEach(linkifyTextNode);
      container.setAttribute(processedAttr, '1');
    };
    const containers = new Set();
    if (scope.matches && scope.matches(containerSelector)) {
      containers.add(scope);
    }
    if (scope.closest) {
      const closestContainer = scope.closest(containerSelector);
      if (closestContainer) containers.add(closestContainer);
    }
    if (scope.querySelectorAll) {
      scope.querySelectorAll(containerSelector).forEach(node => containers.add(node));
    }
    containers.forEach(processContainer);
    }, () => startupPerfDebug.summarizeRoot(root || document));
  }

  /* --------------------------------------------------
   * tag 22. 检查更新
   * -------------------------------------------------- */
  const UPDATE_CHECK_KEY = 'xdex_update_check_state';
  const UPDATE_EXTENSION_CHECK_KEY = 'xdex_extension_update_check_state';
  const UPDATE_GREASYFORK_META_URL = 'https://update.greasyfork.org/scripts/531005/X%E5%B2%9B-EX.meta.js';
  const UPDATE_SCRIPTCAT_API_URL = 'https://scriptcat.org/api/v2/scripts/6289';
  const UPDATE_EXTENSION_GITHUB_JSON_URL = 'https://raw.githubusercontent.com/SayaGoodBye/nmbxd-EX/main/nmbxd-EX-Extension/update.json';
  const UPDATE_EXTENSION_JSDELIVR_JSON_URL = 'https://fastly.jsdelivr.net/gh/SayaGoodBye/nmbxd-EX@main/nmbxd-EX-Extension/update.json';
  const UPDATE_CHECK_HOUR = 11;
  function normalizeMetaChangelog(text) {
    return String(text || '')
      .replace(/\\r\\n/g, '\n')
      .replace(/\\n/g, '\n')
      .replace(/\\r/g, '\n')
      .trim();
  }
  function parseVersionAndChangelogFromMeta(metaText) {
    const text = String(metaText || '');
    const versionMatch = text.match(/^\/\/\s*@version\s+(.+)$/m);
    const changelogMatches = [...text.matchAll(/^\/\/\s*@changelog\s+(.+)$/gm)];
    const changelog = normalizeMetaChangelog(changelogMatches
      .map(m => String(m[1] || '').trim())
      .filter(Boolean)
      .join('\n')
      .trim());
    return {
      version: versionMatch ? String(versionMatch[1] || '').trim() : '',
      changelog
    };
  }
  const CHANGELOG = parseVersionAndChangelogFromMeta(GM_info.scriptMetaStr || '').changelog || '';
  function getUpdateCheckStorageKey() {
    return XDEX_RUNTIME && XDEX_RUNTIME.kind === 'extension' ? UPDATE_EXTENSION_CHECK_KEY : UPDATE_CHECK_KEY;
  }
  function getDefaultUpdateCheckState() {
    return {
      lastCheckDate: '',
      nextCheckAt: 0,
      pendingUpdateVersion: '',
      pendingUpdateChangelog: '',
      pendingUpdateSource: '',
      pendingUpdateDetectedAt: 0,
      latestRemoteVersion: '',
      ignoredVersion: '',
      lastDismissDate: '',
      dismissedUntil: 0
    };
  }
  function getUpdateCheckState() {
    try {
      const saved = GM_getValue(getUpdateCheckStorageKey(), null);
      const merged = Object.assign(getDefaultUpdateCheckState(), saved || {});
      console.log('[update-check] get state:', merged);
      return merged;
    } catch (e) {
      console.warn('[update-check] get state failed, fallback to default:', e);
      return getDefaultUpdateCheckState();
    }
  }
  function setUpdateCheckState(nextState) {
    const merged = Object.assign(getDefaultUpdateCheckState(), nextState || {});
    GM_setValue(getUpdateCheckStorageKey(), merged);
    console.log('[update-check] set state:', merged);
    return merged;
  }
  function formatLocalDateKey(ts = Date.now()) {
    const d = new Date(ts);
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    return `${y}-${m}-${day}`;
  }
  function getNextNaturalCheckAt(nowTs = Date.now(), hour = UPDATE_CHECK_HOUR) {
    const d = new Date(nowTs);
    d.setDate(d.getDate() + 1);
    d.setHours(hour, 0, 0, 0);
    return d.getTime();
  }
  function compareVersionStrings(a, b) {
    const pa = String(a || '').split('.').map(v => parseInt(v, 10) || 0);
    const pb = String(b || '').split('.').map(v => parseInt(v, 10) || 0);
    const len = Math.max(pa.length, pb.length);
    for (let i = 0; i < len; i++) {
      const av = pa[i] || 0;
      const bv = pb[i] || 0;
      if (av > bv) return 1;
      if (av < bv) return -1;
    }
    return 0;
  }

  async function fetchMetaVersionAndChangelog(url, source) {
    const resp = await gmRequest(url, 'text');
    const parsed = parseVersionAndChangelogFromMeta(resp.responseText || '');
    return {
      source,
      url,
      version: parsed.version,
      changelog: parsed.changelog
    };
  }
  async function fetchScriptCatVersionAndChangelog(url, source = 'scriptcat') {
    const resp = await gmRequest(url, 'text');
    const json = JSON.parse(resp.responseText || '{}');
    const script = (json && json.data && json.data.script) || {};
    return {
      source,
      url,
      version: String(script.version || '').trim(),
      changelog: String(script.changelog || '').trim()
    };
  }
  async function fetchExtensionUpdateJson(url, source) {
    const resp = await gmRequest(url, 'text');
    const json = JSON.parse(resp.responseText || '{}');
    const extension = json && json.extension ? json.extension : json;
    return {
      source,
      url,
      version: String(extension.version || '').trim(),
      changelog: String(extension.changelog || '').trim(),
      downloads: extension.downloads || {}
    };
  }
  function getUpdateCheckRequestsForRuntime() {
    if (XDEX_RUNTIME && XDEX_RUNTIME.kind === 'extension') {
      return [
        () => fetchExtensionUpdateJson(UPDATE_EXTENSION_GITHUB_JSON_URL, 'github'),
        () => fetchExtensionUpdateJson(UPDATE_EXTENSION_JSDELIVR_JSON_URL, 'jsdelivr')
      ];
    }
    return [
      () => fetchMetaVersionAndChangelog(UPDATE_GREASYFORK_META_URL, 'greasyfork'),
      () => fetchScriptCatVersionAndChangelog(UPDATE_SCRIPTCAT_API_URL, 'scriptcat')
    ];
  }
  function choosePreferredRemoteMeta(results) {
    const valid = (results || []).filter(item => item && item.version);
    if (!valid.length) return null;
    valid.sort((a, b) => compareVersionStrings(b.version, a.version));
    const topVersion = valid[0].version;
    const topCandidates = valid.filter(item => compareVersionStrings(item.version, topVersion) === 0);
    const preferredSource = XDEX_RUNTIME && XDEX_RUNTIME.kind === 'extension' ? 'github' : 'greasyfork';
    const preferred = topCandidates.find(item => item.source === preferredSource) || topCandidates[0];
    return preferred;
  }
  function shouldShowPendingUpdateReminder(state, currentVersion = VERSION) {
    if (!state || !state.pendingUpdateVersion) return false;
    if (compareVersionStrings(state.pendingUpdateVersion, currentVersion) <= 0) return false;
    if (state.ignoredVersion && state.ignoredVersion === state.pendingUpdateVersion) return false;
    if (state.lastDismissDate && state.lastDismissDate === formatLocalDateKey()) return false;
    if (state.dismissedUntil && Date.now() < state.dismissedUntil) return false;
    return true;
  }
  function updateSettingsButtonBadge(state = getUpdateCheckState()) {
    const $btn = $('#sp_btn');
    if (!$btn.length) return;
    if (typeof isUpdateCheckEnabled === 'function' && !isUpdateCheckEnabled()) {
      $btn.removeClass('xdex-has-update');
      return;
    }
    $btn.toggleClass('xdex-has-update', shouldShowPendingUpdateReminder(state));
  }
  function clearFooterUpdateHighlight() {
    const $links = $('#sp_panel_footer .sp_panel_links');
    $links.removeClass('xdex-update-highlight xdex-update-source-greasyfork xdex-update-source-scriptcat xdex-update-source-github');
    $links.find('[data-update-channel]').removeClass('xdex-update-link-primary xdex-update-link-secondary');
  }
  function flashFooterUpdateHighlight(source = '') {
    const $links = $('#sp_panel_footer .sp_panel_links');
    if (!$links.length) return;
    clearFooterUpdateHighlight();
    $links.addClass('xdex-update-highlight');
    const sourceKey = String(source || '').trim().toLowerCase();
    const channelMap = {
      greasyfork: {
        containerClass: 'xdex-update-source-greasyfork',
        primary: 'greasyfork',
        secondary: ['github']
      },
      scriptcat: {
        containerClass: 'xdex-update-source-scriptcat',
        primary: 'scriptcat',
        secondary: ['baidupan']
      },
      github: {
        containerClass: 'xdex-update-source-github',
        primary: 'github',
        secondary: ['baidupan']
      },
      jsdelivr: {
        containerClass: 'xdex-update-source-github',
        primary: 'baidupan',
        secondary: ['github']
      }
    };
    const config = channelMap[sourceKey];
    if (config) {
      $links.addClass(config.containerClass);
      $links.find(`[data-update-channel="${config.primary}"]`).addClass('xdex-update-link-primary');
      (config.secondary || []).forEach((channel) => {
        $links.find(`[data-update-channel="${channel}"]`).addClass('xdex-update-link-secondary');
      });
    }
    setTimeout(() => {
      clearFooterUpdateHighlight();
    }, 5000);
  }
  function renderUpdateLogDialog(mode = 'local', state = getUpdateCheckState()) {
    const $dlg = $('#sp_update_log');
    if (!$dlg.length) return;
    const isRemote = mode === 'remote' && state && state.pendingUpdateVersion && compareVersionStrings(state.pendingUpdateVersion, VERSION) > 0;
    const title = isRemote ? `发现新版本 v${state.pendingUpdateVersion}` : '更新日志';
    const bodyText = isRemote
      ? (state.pendingUpdateChangelog || `发现新版本 v${state.pendingUpdateVersion},但未提取到更新说明。`)
      : (CHANGELOG || '暂无更新说明');
    $dlg.attr('data-update-mode', isRemote ? 'remote' : 'local');
    $dlg.find('.xdex-update-log-title').text(title);
    $dlg.find('.xdex-update-log-body').text(bodyText);
    $dlg.find('.xdex-update-log-actions').css('display', isRemote ? 'flex' : 'none');
  }
  function openUpdateLogDialog(mode = 'local') {
    const state = getUpdateCheckState();
    renderUpdateLogDialog(mode, state);
    $('#sp_update_log').fadeIn(120);
  }
  function closeUpdateLogDialog(options = {}) {
    const { treatAsDismiss = false, reason = 'unknown' } = options || {};
    const mode = $('#sp_update_log').attr('data-update-mode') || '';
    if (treatAsDismiss) {
      const state = getUpdateCheckState();
      state.lastDismissDate = formatLocalDateKey();
      state.dismissedUntil = state.nextCheckAt || getNextNaturalCheckAt();
      setUpdateCheckState(state);
      updateSettingsButtonBadge(state);
      console.log('[update-check] dismiss dialog:', {
        reason,
        mode,
        treatAsDismiss: true,
        lastDismissDate: state.lastDismissDate,
        dismissedUntil: state.dismissedUntil,
        dismissedUntilISO: state.dismissedUntil ? new Date(state.dismissedUntil).toISOString() : ''
      });
    } else {
      console.log('[update-check] close dialog:', {
        reason,
        mode,
        treatAsDismiss: false
      });
    }
    $('#sp_update_log').fadeOut(120);
  }
  function maybeShowPendingUpdateDialogOnPanelOpen() {
    if (!isUpdateCheckEnabled()) {
      const state = getDefaultUpdateCheckState();
      updateSettingsButtonBadge(state);
      clearFooterUpdateHighlight();
      return;
    }
    const state = getUpdateCheckState();
    updateSettingsButtonBadge(state);
    if (shouldShowPendingUpdateReminder(state)) {
      openUpdateLogDialog('remote');
    }
  }
  async function checkForDailyScriptUpdate(force = false) {
    if (!isUpdateCheckEnabled()) {
      const state = getUpdateCheckState();
      updateSettingsButtonBadge(state);
      clearFooterUpdateHighlight();
      return state;
    }
    const now = Date.now();
    const today = formatLocalDateKey(now);
    const state = getUpdateCheckState();
    const alreadyChecked = !force && state.nextCheckAt && now < state.nextCheckAt;
    console.log('[update-check] start:', {
      force,
      now,
      today,
      currentVersion: VERSION,
      state: Object.assign({}, state),
      alreadyChecked,
      nextCheckAtISO: state.nextCheckAt ? new Date(state.nextCheckAt).toISOString() : ''
    });
    if (alreadyChecked) {
      console.log('[update-check] skip: already checked for current window', {
        force,
        now,
        nextCheckAt: state.nextCheckAt,
        nextCheckAtISO: new Date(state.nextCheckAt).toISOString()
      });
      updateSettingsButtonBadge(state);
      return state;
    }
    state.lastCheckDate = today;
    state.nextCheckAt = getNextNaturalCheckAt(now);
    console.log('[update-check] scheduled next check:', {
      lastCheckDate: state.lastCheckDate,
      nextCheckAt: state.nextCheckAt,
      nextCheckAtISO: new Date(state.nextCheckAt).toISOString()
    });
    try {
      const requests = getUpdateCheckRequestsForRuntime();
      const settled = await Promise.allSettled(requests.map((request) => request()));
      const sourceResults = settled.map((item, index) => {
        const source = XDEX_RUNTIME && XDEX_RUNTIME.kind === 'extension'
          ? (index === 0 ? 'github' : 'jsdelivr')
          : (index === 0 ? 'greasyfork' : 'scriptcat');
        if (item.status === 'fulfilled') {
          console.log('[update-check] remote meta success:', item.value);
          return item.value;
        }
        console.warn(`[update-check] remote meta failed: ${source}`, item.reason);
        return null;
      });
      const preferredRemote = choosePreferredRemoteMeta(sourceResults);
      console.log('[update-check] remote meta choice:', {
        localVersion: VERSION,
        candidates: sourceResults,
        preferred: preferredRemote
      });
      const remoteVersion = preferredRemote ? String(preferredRemote.version || '').trim() : '';
      const remoteChangelog = preferredRemote ? String(preferredRemote.changelog || '').trim() : '';
      state.latestRemoteVersion = remoteVersion;
      if (remoteVersion && compareVersionStrings(remoteVersion, VERSION) > 0) {
        state.pendingUpdateVersion = remoteVersion;
        state.pendingUpdateDetectedAt = now;
        state.pendingUpdateChangelog = remoteChangelog || `发现新版本 v${remoteVersion},但未提取到更新说明。`;
        state.pendingUpdateSource = preferredRemote ? String(preferredRemote.source || '').trim() : '';
        if (state.dismissedUntil && now >= state.dismissedUntil) {
          state.dismissedUntil = 0;
          state.lastDismissDate = '';
        }
        if (state.ignoredVersion && compareVersionStrings(state.ignoredVersion, remoteVersion) < 0) {
          state.ignoredVersion = '';
        }
      } else {
        state.pendingUpdateVersion = '';
        state.pendingUpdateChangelog = '';
        state.pendingUpdateSource = '';
        state.pendingUpdateDetectedAt = 0;
      }
    } catch (e) {
      console.warn('[update-check] daily update check failed:', e);
    }
    console.log('[update-check] final state before save:', state);
    setUpdateCheckState(state);
    updateSettingsButtonBadge(state);
    return state;
  }

  function initializeUpdateReminderUI() {
    updateSettingsButtonBadge(getUpdateCheckState());
    clearFooterUpdateHighlight();
  }
  window.__xdexGetUpdateCheckState = function() {
    return getUpdateCheckState();
  };
  window.__xdexCheckUpdateNow = function() {
    return checkForDailyScriptUpdate(true).then((state) => {
      scheduleNextUpdateCheckTimer(state);
      return state;
    });
  };
  window.__xdexClearUpdateCheckState = function() {
    GM_deleteValue(UPDATE_CHECK_KEY);
    const state = getDefaultUpdateCheckState();
    updateSettingsButtonBadge(state);
    clearFooterUpdateHighlight();
    console.log('[update-check] cleared state');
    return state;
  };
  window.__xdexSetUpdateCheckState = function(nextState) {
    const state = setUpdateCheckState(nextState || {});
    updateSettingsButtonBadge(state);
    scheduleNextUpdateCheckTimer(state);
    return state;
  };
  window.__xdexPatchUpdateCheckState = function(patch) {
    const state = Object.assign(getUpdateCheckState(), patch || {});
    return window.__xdexSetUpdateCheckState(state);
  };
  const updateDebugTarget = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
  updateDebugTarget.__xdexGetUpdateCheckState = window.__xdexGetUpdateCheckState;
  updateDebugTarget.__xdexCheckUpdateNow = window.__xdexCheckUpdateNow;
  updateDebugTarget.__xdexClearUpdateCheckState = window.__xdexClearUpdateCheckState;
  updateDebugTarget.__xdexSetUpdateCheckState = window.__xdexSetUpdateCheckState;
  updateDebugTarget.__xdexPatchUpdateCheckState = window.__xdexPatchUpdateCheckState;

  function scheduleDailyUpdateCheck() {
    if (!isUpdateCheckEnabled()) {
      scheduleNextUpdateCheckTimer(getUpdateCheckState());
      return;
    }
    checkForDailyScriptUpdate(false)
      .then((state) => {
        scheduleNextUpdateCheckTimer(state);
        if (shouldShowPendingUpdateReminder(state) && $('#sp_cover').is(':visible')) {
          openUpdateLogDialog('remote');
        }
      })
      .catch((e) => {
        console.warn('[update-check] schedule failed:', e);
      });
  }
  function scheduleNextUpdateCheckTimer(state = getUpdateCheckState()) {
    if (window.__xdexUpdateCheckTimer) {
      clearTimeout(window.__xdexUpdateCheckTimer);
      window.__xdexUpdateCheckTimer = 0;
    }
    if (!isUpdateCheckEnabled()) return;
    const nextCheckAt = Number(state && state.nextCheckAt || 0);
    if (!nextCheckAt) return;
    const delay = Math.max(0, nextCheckAt - Date.now());
    window.__xdexUpdateCheckTimer = setTimeout(() => {
      window.__xdexUpdateCheckTimer = 0;
      scheduleDailyUpdateCheck();
    }, Math.min(delay, 2147483647));
    console.log('[update-check] timer scheduled:', {
      nextCheckAt,
      nextCheckAtISO: new Date(nextCheckAt).toISOString(),
      delay
    });
  }

  /* --------------------------------------------------
   * tag 23. 图片/GIF 右键菜单增强
   * -------------------------------------------------- */
  function ensureImageContextMenuStyle() {
    if (document.getElementById('xdex-image-context-menu-style')) return;
    const style = document.createElement('style');
    style.id = 'xdex-image-context-menu-style';
    style.textContent = `
      #xdex-image-context-menu {
        position: fixed;
        z-index: 10050;
        min-width: 156px;
        max-width: 220px;
        padding: 6px 0;
        background: #FFFFEE;
        border: 1px solid #d6c7ba;
        border-radius: 10px;
        box-shadow: 0 8px 24px rgba(0,0,0,0.18);
        font-size: 13px;
        color: #333;
        user-select: none;
      }
      :root.xdex-darkreader-active #xdex-image-context-menu {
        background: #2b2c2d;
        border-color: #4b4d50;
        box-shadow: 0 10px 28px rgba(0,0,0,0.5);
        color: #e6e6e6;
      }
      #xdex-image-context-menu.xdex-hidden {
        display: none;
      }
      #xdex-image-context-menu .xdex-image-context-item {
        display: flex;
        align-items: center;
        width: 100%;
        padding: 8px 12px;
        border: 0;
        background: transparent;
        color: inherit;
        text-align: left;
        cursor: pointer;
        line-height: 1.35;
        box-sizing: border-box;
      }
      #xdex-image-context-menu .xdex-image-context-item:hover,
      #xdex-image-context-menu .xdex-image-context-item:focus {
        background: rgba(102, 204, 255, 0.14);
        outline: none;
      }
      :root.xdex-darkreader-active #xdex-image-context-menu .xdex-image-context-item:hover,
      :root.xdex-darkreader-active #xdex-image-context-menu .xdex-image-context-item:focus {
        background: rgba(102, 204, 255, 0.18);
      }
      #xdex-image-context-menu .xdex-image-context-sep {
        height: 1px;
        margin: 4px 0;
        background: rgba(0,0,0,0.08);
      }
      :root.xdex-darkreader-active #xdex-image-context-menu .xdex-image-context-sep {
        background: rgba(255,255,255,0.08);
      }
      #xdex-image-context-menu .xdex-image-context-hint {
        display: block;
        margin-top: 2px;
        font-size: 11px;
        opacity: 0.72;
      }
    `;
    document.head.appendChild(style);
  }
  function gmRequestBlob(url) {
    return new Promise((resolve, reject) => {
      if (!url || typeof GM_xmlhttpRequest !== 'function') {
        reject(new Error('GM_xmlhttpRequest 不可用'));
        return;
      }
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        responseType: 'blob',
        onload: (res) => {
          if (res.status >= 200 && res.status < 300 && res.response) {
            resolve(res.response);
          } else {
            reject(new Error(`请求失败: ${res.status} ${url}`));
          }
        },
        onerror: (err) => {
          const reason = err && (err.error || err.statusText || err.status || err.message);
          reject(new Error(`请求失败: ${reason || '未知错误'} ${url}`));
        },
        ontimeout: () => reject(new Error('请求超时'))
      });
    });
  }
  async function fetchImageBlobByBrowser(url) {
    const resp = await fetch(url, { credentials: 'omit', cache: 'no-store' });
    if (!resp.ok) throw new Error(`fetch 请求失败: ${resp.status} ${url}`);
    return await resp.blob();
  }
  function handleImageContextMenuEvent(e, layer) {
    if (layer === 'document' && e.defaultPrevented) return false;
    // console.info('【右键菜单|收到右键】', {
    //   标签: e.target && e.target.tagName,
    //   类名: e.target && e.target.className ? String(e.target.className).slice(0, 120) : '',
    //   坐标: `${e.clientX},${e.clientY}`,
    //   层级: layer
    // });
    const anchor = e.target && e.target.closest ? e.target.closest('.h-threads-img-a') : null;
    if (!anchor) {
      // console.info('【右键菜单|跳过】', { 原因: '未命中图片链接', 层级: layer });
      return false;
    }
    if (anchor.closest('.h-preview-box')) {
      // console.info('【右键菜单|跳过】', { 原因: '命中预览框图片', 层级: layer });
      return false;
    }
    // console.info('【右键菜单|命中】', { 链接: (anchor.href || '').slice(0, 200), 层级: layer });
    const ctx = resolveImageContextData(anchor);
    if (!ctx) {
      console.warn('【右键菜单|解析失败】', { 原因: '图片上下文为空', 层级: layer });
      return false;
    }
    e.preventDefault();
    if (typeof e.stopImmediatePropagation === 'function') {
      e.stopImmediatePropagation();
    }
    e.stopPropagation();
    e.cancelBubble = true;
    e.returnValue = false;
    // console.info('【右键菜单|阻止原生】', { 成功: true, 层级: layer });
    openImageContextMenu(ctx, e.clientX, e.clientY);
    return true;
  }
  function getImageContextMenu() {
    ensureImageContextMenuStyle();
    let menu = document.getElementById('xdex-image-context-menu');
    // if (menu) console.debug('【右键菜单|复用】', { 已存在: true });
    if (menu) return menu;
    // console.debug('【右键菜单|创建】', { 已存在: false });
    menu = document.createElement('div');
    menu.id = 'xdex-image-context-menu';
    menu.className = 'xdex-hidden';
    menu.innerHTML = [
      // '<button type="button" class="xdex-image-context-item" data-action="copy-image">复制图片<span class="xdex-image-context-hint">GIF 先尝试原样复制,失败时自动降级</span></button>',
      '<button type="button" class="xdex-image-context-item" data-action="copy-image">复制图片</button>',
      '<button type="button" class="xdex-image-context-item" data-action="download-image">下载图片</button>',
      '<button type="button" class="xdex-image-context-item" data-action="copy-image-url">复制图片链接</button>',
      // '<div class="xdex-image-context-sep"></div>',
      '<button type="button" class="xdex-image-context-item" data-action="copy-thread-link">复制串链接</button>'
    ].join('');
    menu.__context = null;
    menu.addEventListener('click', async (e) => {
      const btn = e.target.closest('.xdex-image-context-item[data-action]');
      if (!btn) return;
      e.preventDefault();
      const ctx = menu.__context;
      closeImageContextMenu();
      if (!ctx) return;
      const action = btn.getAttribute('data-action');
      try {
        if (action === 'copy-image') {
          await copyImageBinary(ctx);
        } else if (action === 'download-image') {
          await downloadImageBlob(ctx);
        } else if (action === 'copy-image-url') {
          await copyImageUrl(ctx);
        } else if (action === 'copy-thread-link') {
          await copyThreadLink(ctx);
        }
      } catch (err) {
        console.warn('[tag23] action failed:', action, err);
        toast('操作失败,请重试');
      }
    });
    document.body.appendChild(menu);
    // console.info('【右键菜单|挂载】', { 位置: 'body', 菜单项数: menu.querySelectorAll('.xdex-image-context-item').length });
    if (!getImageContextMenu.__globalBound) {
      // console.info('【右键菜单|注册】', { 类型: 'contextmenu/click/blur/scroll/keydown', 阶段: 'capture' });
      window.addEventListener('contextmenu', (e) => {
        return handleImageContextMenuEvent(e, 'window') ? false : undefined;
      }, true);
      document.addEventListener('contextmenu', (e) => {
        return handleImageContextMenuEvent(e, 'document') ? false : undefined;
      }, true);
      document.addEventListener('click', (e) => {
        const cur = document.getElementById('xdex-image-context-menu');
        if (!cur || cur.classList.contains('xdex-hidden')) return;
        if (cur.contains(e.target)) return;
        closeImageContextMenu();
      }, true);
      window.addEventListener('blur', () => closeImageContextMenu());
      window.addEventListener('scroll', () => closeImageContextMenu(), true);
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') closeImageContextMenu();
      });
      getImageContextMenu.__globalBound = true;
    }
    return menu;
  }
  function closeImageContextMenu() {
    const menu = document.getElementById('xdex-image-context-menu');
    if (!menu) return;
    menu.classList.add('xdex-hidden');
    menu.style.left = '-9999px';
    menu.style.top = '-9999px';
    menu.__context = null;
    // console.debug('【右键菜单|关闭】', { 原因: '隐藏菜单' });
  }
  function shouldUseExtensionNativeImageContextMenu(cfg) {
    return !!(XDEX_RUNTIME && XDEX_RUNTIME.kind === 'extension' && cfg && cfg.enableImageContextMenu);
  }
  function openImageContextMenu(context, x, y) {
    const menu = getImageContextMenu();
    menu.__context = context;
    menu.classList.remove('xdex-hidden');
    menu.style.left = '0px';
    menu.style.top = '0px';
    const rect = menu.getBoundingClientRect();
    const left = Math.min(x, window.innerWidth - rect.width - 8);
    const top = Math.min(y, window.innerHeight - rect.height - 8);
    menu.style.left = Math.max(8, left) + 'px';
    menu.style.top = Math.max(8, top) + 'px';
    // console.info('【右键菜单|显示】', {
    //   位置: `${menu.style.left},${menu.style.top}`,
    //   图片地址: String(context && context.imageUrl || '').slice(0, 200),
    //   GIF: !!(context && context.isGif)
    // });
  }
  function resolveThreadLinkFromImageTarget(target) {
    const $target = $(target);
    const tid = getThreadIdForElement($target);
    if (tid) return `${location.origin}/t/${tid}`;
    return location.href;
  }
  function sanitizeImageFileName(name) {
    return String(name || 'image')
      .replace(/[\\/:*?"<>|]+/g, '_')
      .replace(/\s+/g, ' ')
      .trim() || 'image';
  }
  function resolveImageContextData(anchorOrBox) {
    const anchor = anchorOrBox && anchorOrBox.matches && anchorOrBox.matches('.h-threads-img-a')
      ? anchorOrBox
      : anchorOrBox && anchorOrBox.querySelector
        ? anchorOrBox.querySelector('.h-threads-img-a')
        : null;
    if (!anchor) return null;
    const imgBox = anchor.closest('.h-threads-img-box');
    const img = imgBox ? imgBox.querySelector('.h-threads-img') : null;
    const rawUrl = (anchor.href || (img && (img.currentSrc || img.src)) || '').trim();
    if (!rawUrl) {
      console.warn('【右键菜单|解析跳过】', { 原因: '图片地址为空' });
      return null;
    }
    const cleanUrl = rawUrl.replace('/thumb/', '/image/');
    let fileName = '';
    try {
      const u = new URL(cleanUrl, location.href);
      const leaf = decodeURIComponent((u.pathname.split('/').pop() || '').trim());
      fileName = sanitizeImageFileName(leaf || `image-${Date.now()}`);
    } catch (e) {
      fileName = sanitizeImageFileName(`image-${Date.now()}`);
    }
    return {
      anchor,
      imgBox,
      img,
      imageUrl: cleanUrl,
      imageUrlRaw: rawUrl,
      threadUrl: resolveThreadLinkFromImageTarget(anchor),
      fileName,
      isGif: /\.gif(?:$|[?#])/i.test(cleanUrl)
    };
  }
  async function writeClipboardText(text, successToast) {
    await navigator.clipboard.writeText(String(text || ''));
    if (successToast) toast(successToast);
  }
  function blobToObjectUrlDownload(blob, fileName) {
    const objectUrl = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = objectUrl;
    a.download = sanitizeImageFileName(fileName);
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    setTimeout(() => URL.revokeObjectURL(objectUrl), 3000);
  }
  function resolveImageMimeType(ctx, blob) {
    let type = String(blob && blob.type || '').split(';')[0].trim().toLowerCase();
    if (type === 'image/jpg') type = 'image/jpeg';
    if (type) return type;
    const source = `${ctx && ctx.imageUrl || ''} ${ctx && ctx.fileName || ''}`;
    if (/\.gif(?:$|[?#\s])/i.test(source)) return 'image/gif';
    if (/\.jpe?g(?:$|[?#\s])/i.test(source)) return 'image/jpeg';
    if (/\.png(?:$|[?#\s])/i.test(source)) return 'image/png';
    if (/\.webp(?:$|[?#\s])/i.test(source)) return 'image/webp';
    if (/\.bmp(?:$|[?#\s])/i.test(source)) return 'image/bmp';
    if (/\.avif(?:$|[?#\s])/i.test(source)) return 'image/avif';
    return '';
  }
  async function detectImageMimeTypeFromMagic(blob) {
    if (!blob || blob.size < 4) return '';
    const bytes = new Uint8Array(await blob.slice(0, 16).arrayBuffer());
    if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38) return 'image/gif';
    if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'image/jpeg';
    if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 && bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) return 'image/png';
    if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) return 'image/webp';
    if (bytes[0] === 0x42 && bytes[1] === 0x4d) return 'image/bmp';
    return '';
  }
  async function fetchImageBlobForContext(ctx) {
    let blob;
    try {
      blob = await gmRequestBlob(ctx.imageUrl);
    } catch (err) {
      console.warn('【右键菜单|失败】', { 动作: '抓取图片', 阶段: 'GM_xmlhttpRequest', 原因: err && err.message ? err.message : String(err) });
      blob = await fetchImageBlobByBrowser(ctx.imageUrl);
      console.info('【右键菜单|抓取图片】', { 阶段: 'fetch备用成功', 地址: String(ctx && ctx.imageUrl || '').slice(0, 200), MIME: String(blob && blob.type || ''), 大小: blob && blob.size });
    }
    const type = resolveImageMimeType(ctx, blob);
    if (!blob || !type.startsWith('image/')) {
      throw new Error(`Not an image content type. Got: ${blob && blob.type}`);
    }
    return blob;
  }
  async function convertBlobToPng(blob) {
    const bitmap = await createImageBitmap(blob);
    const canvas = document.createElement('canvas');
    canvas.width = bitmap.width;
    canvas.height = bitmap.height;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(bitmap, 0, 0);
    bitmap.close();
    return await new Promise((resolve, reject) => {
      canvas.toBlob((pngBlob) => {
        if (pngBlob) resolve(pngBlob);
        else reject(new Error('Canvas toBlob failed'));
      }, 'image/png');
    });
  }
  function createClipboardBlob(data, type) {
    return new Blob([data], { type });
  }
  function escapeHtmlAttr(value) {
    return String(value || '').replace(/[&<>"]/g, (ch) => ({
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;'
    })[ch]);
  }
  function escapeHtml(value) {
    return String(value || '').replace(/[&<>]/g, (ch) => ({
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;'
    })[ch]);
  }
  async function isAnimatedPngBlob(blob) {
    if (!blob || blob.size < 33) return false;
    const header = new Uint8Array(await blob.slice(0, Math.min(blob.size, 1024 * 1024)).arrayBuffer());
    const pngSignature = [137, 80, 78, 71, 13, 10, 26, 10];
    for (let i = 0; i < pngSignature.length; i++) {
      if (header[i] !== pngSignature[i]) return false;
    }
    for (let offset = 8; offset + 12 <= header.length;) {
      const length = ((header[offset] << 24) | (header[offset + 1] << 16) | (header[offset + 2] << 8) | header[offset + 3]) >>> 0;
      const type = String.fromCharCode(header[offset + 4], header[offset + 5], header[offset + 6], header[offset + 7]);
      if (type === 'acTL') return true;
      if (type === 'IDAT' || type === 'IEND') return false;
      offset += 12 + length;
      if (offset > header.length) return false;
    }
    return false;
  }
  async function copyAnimatedPngBinary(ctx, blob) {
    const imageUrl = String(ctx && ctx.imageUrl || '');
    const pngBlob = blob.type === 'image/png' ? blob : new Blob([blob], { type: 'image/png' });
    const webPngSupports = typeof ClipboardItem.supports === 'function' ? ClipboardItem.supports('web image/png') : undefined;
    console.info('【右键菜单|APNG复制】', {
      阶段: '开始',
      大小: pngBlob.size,
      原始MIME: String(blob.type || ''),
      webImagePngSupports: webPngSupports,
      地址: imageUrl.slice(0, 200)
    });
    try {
      const html = `<img src="${escapeHtmlAttr(imageUrl)}">`;
      await navigator.clipboard.write([new ClipboardItem({
        'web image/png': pngBlob,
        'text/html': createClipboardBlob(html, 'text/html'),
        'text/plain': createClipboardBlob(imageUrl, 'text/plain')
      })]);
      toast('APNG 已使用富文本图片格式复制到剪贴板');
      console.info('【右键菜单|APNG复制】', { 阶段: 'web image/png成功', 附带: 'text/html,text/plain' });
      return true;
    } catch (err) {
      console.warn('【右键菜单|失败】', { 动作: '复制APNG', 阶段: 'web image/png写入', 原因: err && err.message ? err.message : String(err) });
    }
    try {
      const html = `<img src="${escapeHtmlAttr(imageUrl)}">`;
      await navigator.clipboard.write([new ClipboardItem({
        'text/html': createClipboardBlob(html, 'text/html'),
        'text/plain': createClipboardBlob(imageUrl, 'text/plain')
      })]);
      toast('APNG 已使用富文本图片格式复制到剪贴板');
      console.info('【右键菜单|APNG复制】', { 阶段: 'text/html成功' });
      return true;
    } catch (err) {
      console.warn('【右键菜单|失败】', { 动作: '复制APNG', 阶段: 'text/html写入', 原因: err && err.message ? err.message : String(err) });
    }
    await copyImageUrl(ctx);
    toast('当前环境无法保留 APNG 动画,已复制图片地址');
    console.info('【右键菜单|APNG复制】', { 阶段: 'URL兜底' });
    return false;
  }
  async function copyGifBinary(ctx, blob) {
    const gifUrl = String(ctx && ctx.imageUrl || '');
    const gifBlob = blob.type === 'image/gif' ? blob : new Blob([blob], { type: 'image/gif' });
    const gifSupports = typeof ClipboardItem.supports === 'function' ? ClipboardItem.supports('image/gif') : undefined;
    const webGifSupports = typeof ClipboardItem.supports === 'function' ? ClipboardItem.supports('web image/gif') : undefined;
    console.info('【右键菜单|GIF复制】', {
      阶段: '开始',
      大小: gifBlob.size,
      原始MIME: String(blob.type || ''),
      imageGifSupports: gifSupports,
      webImageGifSupports: webGifSupports,
      地址: gifUrl.slice(0, 200)
    });
    if (gifSupports === true) {
      try {
        await navigator.clipboard.write([new ClipboardItem({ 'image/gif': gifBlob })]);
        toast('GIF 已原样复制到剪贴板');
        console.info('【右键菜单|GIF复制】', { 阶段: 'image/gif成功' });
        return true;
      } catch (err) {
        console.warn('【右键菜单|失败】', { 动作: '复制GIF', 阶段: 'image/gif写入', 原因: err && err.message ? err.message : String(err) });
      }
    } else {
      console.info('【右键菜单|GIF复制】', { 阶段: 'image/gif跳过', 原因: 'ClipboardItem.supports 未确认支持' });
    }
    try {
      const html = `<img src="${escapeHtmlAttr(gifUrl)}">`;
      const data = {
        'web image/gif': gifBlob,
        'text/html': createClipboardBlob(html, 'text/html'),
        'text/plain': createClipboardBlob(gifUrl, 'text/plain')
      };
      await navigator.clipboard.write([new ClipboardItem(data)]);
      toast('GIF 已使用富文本图片格式复制到剪贴板');
      console.info('【右键菜单|GIF复制】', { 阶段: 'web image/gif成功', 附带: 'text/html,text/plain' });
      return true;
    } catch (err) {
      console.warn('【右键菜单|失败】', { 动作: '复制GIF', 阶段: 'web image/gif写入', 原因: err && err.message ? err.message : String(err) });
    }
    try {
      const html = `<img src="${escapeHtmlAttr(gifUrl)}">`;
      await navigator.clipboard.write([new ClipboardItem({
        'text/html': createClipboardBlob(html, 'text/html'),
        'text/plain': createClipboardBlob(gifUrl, 'text/plain')
      })]);
      toast('GIF 已使用富文本图片格式复制到剪贴板');
      console.info('【右键菜单|GIF复制】', { 阶段: 'text/html成功' });
      return true;
    } catch (err) {
      console.warn('【右键菜单|失败】', { 动作: '复制GIF', 阶段: 'text/html写入', 原因: err && err.message ? err.message : String(err) });
    }
    await copyImageUrl(ctx);
    toast('当前环境无法复制 GIF 二进制,已复制图片地址');
    console.info('【右键菜单|GIF复制】', { 阶段: 'URL兜底' });
    return false;
  }
  async function copyImageBinary(ctx) {
    console.info('【右键菜单|执行】', { 动作: '复制图片', GIF: !!(ctx && ctx.isGif), 地址: String(ctx && ctx.imageUrl || '').slice(0, 200) });
    if (!navigator.clipboard || typeof navigator.clipboard.write !== 'function' || typeof ClipboardItem === 'undefined') {
      await copyImageUrl(ctx);
      toast('当前环境不支持复制图片,已复制图片地址');
      return;
    }
    const blob = await fetchImageBlobForContext(ctx);
    const declaredType = resolveImageMimeType(ctx, blob);
    const magicType = await detectImageMimeTypeFromMagic(blob);
    const type = magicType || declaredType;
    const supportsOriginal = type === 'image/png'
      || (type === 'image/svg+xml' && typeof ClipboardItem.supports === 'function' && ClipboardItem.supports(type));
    console.info('【右键菜单|复制诊断】', {
      MIME: type,
      原始MIME: String(blob.type || ''),
      魔数MIME: magicType,
      大小: blob.size,
      ClipboardItemSupports: supportsOriginal,
      GIF: !!(ctx && ctx.isGif)
    });
    if (type === 'image/gif') {
      await copyGifBinary(ctx, blob);
      return;
    }
    if (type === 'image/png' && await isAnimatedPngBlob(blob)) {
      await copyAnimatedPngBinary(ctx, blob);
      return;
    }
    try {
      if (supportsOriginal) {
        await navigator.clipboard.write([new ClipboardItem({ [type]: blob })]);
        toast('图片已复制到剪贴板');
        return;
      }
      console.info('【右键菜单|复制诊断】', { 阶段: '原始格式跳过', MIME: type, 原因: '仅 PNG/SVG 作为可靠原格式写入' });
    } catch (err) {
      console.warn('[tag23] native clipboard write failed:', type, err);
      console.warn('【右键菜单|失败】', { 动作: '复制图片', 阶段: '原始格式写入', 原因: err && err.message ? err.message : String(err) });
    }
    if (type !== 'image/gif') {
      try {
        const pngBlob = await convertBlobToPng(blob);
        await navigator.clipboard.write([new ClipboardItem({ 'image/png': pngBlob })]);
        toast('已复制为 PNG ');
        return;
      } catch (err) {
        console.warn('[tag23] png fallback failed:', err);
        console.warn('【右键菜单|失败】', { 动作: '复制图片', 阶段: 'PNG降级', 原因: err && err.message ? err.message : String(err) });
      }
    }
    await copyImageUrl(ctx);
    toast(type === 'image/gif' ? '当前环境无法直接复制 GIF,已复制图片地址' : '当前环境无法直接复制图片,已复制图片地址');
  }
  async function copyImageUrl(ctx) {
    console.info('【右键菜单|执行】', { 动作: '复制图片地址', 地址: String(ctx && ctx.imageUrl || '').slice(0, 200) });
    await writeClipboardText(ctx.imageUrl, '图片地址已复制');
  }
  async function downloadImageBlob(ctx) {
    console.info('【右键菜单|执行】', { 动作: '下载图片', 地址: String(ctx && ctx.imageUrl || '').slice(0, 200), 文件名: ctx && ctx.fileName });
    try {
      const blob = await fetchImageBlobForContext(ctx);
      let fileName = ctx.fileName;
      if (!/\.[a-z0-9]+$/i.test(fileName)) {
        const ext = (blob.type || '').split('/')[1] || 'img';
        fileName = `${fileName}.${ext}`;
      }
      blobToObjectUrlDownload(blob, fileName);
      toast('图片开始下载');
    } catch (err) {
      console.warn('[tag23] download image failed:', err);
      console.warn('【右键菜单|失败】', { 动作: '下载图片', 原因: err && err.message ? err.message : String(err) });
      await copyImageUrl(ctx);
      toast('下载失败,已复制图片地址');
    }
  }
  async function copyThreadLink(ctx) {
    console.info('【右键菜单|执行】', { 动作: '复制串链接', 串链接: String(ctx && ctx.threadUrl || '').slice(0, 200) });
    await writeClipboardText(ctx.threadUrl, '串链接已复制');
  }
  function enableImageContextMenu(root = document) {
    return startupPerfDebug.measure('enableImageContextMenu', () => {
    const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
    if (!cfg.enableImageContextMenu) return;
    if (shouldUseExtensionNativeImageContextMenu(cfg)) return;
    ensureImageContextMenuStyle();
    getImageContextMenu();
    }, () => startupPerfDebug.summarizeRoot(root));
  }

  /* --------------------------------------------------
   * tag 24. 常用串/浏览历史/发言历史/移动端订阅
   * -------------------------------------------------- */

  function isValidThreadId(threadId) {
    return /^\d{6,8}$/.test(threadId);
  }
  function normalizeFavoriteThreadInput(raw) {
    const value = String(raw || '').trim();
    if (isValidThreadId(value)) return value;
    let url;
    try {
      url = new URL(value, location.origin);
    } catch (e) {
      return '';
    }
    if (url.hostname && !['www.nmbxd1.com', 'nmbxd1.com', 'www.nmbxd.com', 'nmbxd.com'].includes(url.hostname)) return '';
    const path = url.pathname || '';
    const threadMatch = path.match(/^\/t\/(\d{6,8})(?:\/\d+)?\/?$/);
    if (threadMatch) return threadMatch[1];
    const poMatch = path.match(/^\/Forum\/po\/id\/(\d{6,8})(?:\/page\/\d+)?(?:\.html)?$/);
    if (poMatch) return poMatch[1];
    return '';
  }
  function makeFavoriteThreadUrl(threadId) {
    return `https://www.nmbxd1.com/t/${threadId}`;
  }
  function trimFavoriteThreadDesc(desc) {
    return String(desc || '').trim().slice(0, 20);
  }
  // 判断码点是否为全角/CJK字符(视觉宽度 2)
  function _isFullWidthCodePoint(cp) {
    return (
      (cp >= 0x1100 && cp <= 0x115F) ||        // Hangul Jamo
      (cp >= 0x2E80 && cp <= 0x303E) ||        // CJK Radicals, Kangxi, etc.
      (cp >= 0x3040 && cp <= 0x33BF) ||        // Hiragana, Katakana, Bopomofo, etc.
      (cp >= 0x3400 && cp <= 0x4DBF) ||        // CJK Extension A
      (cp >= 0x4E00 && cp <= 0x9FFF) ||        // CJK Unified Ideographs
      (cp >= 0xA000 && cp <= 0xA4CF) ||        // Yi Syllables
      (cp >= 0xAC00 && cp <= 0xD7AF) ||        // Hangul Syllables
      (cp >= 0xF900 && cp <= 0xFAFF) ||        // CJK Compatibility Ideographs
      (cp >= 0xFE30 && cp <= 0xFE4F) ||        // CJK Compatibility Forms
      (cp >= 0xFF01 && cp <= 0xFF60) ||        // Fullwidth Forms(含全角字母/数字/标点)
      (cp >= 0xFFE0 && cp <= 0xFFE6) ||        // Fullwidth Signs
      (cp >= 0x20000 && cp <= 0x2FA1F) ||      // CJK Extensions B-F
      cp === 0x2026                             // HORIZONTAL ELLIPSIS(东亚语境占全角宽度)
    );
  }
  // 按视觉宽度截断(CJK/全角=2,其余=1)
  function _truncateByVisualWidth(str, maxWidth) {
    let w = 0;
    for (let i = 0; i < str.length; i++) {
      const cp = str.codePointAt(i);
      const cw = _isFullWidthCodePoint(cp) ? 2 : 1;
      if (w + cw > maxWidth) return str.slice(0, i);
      w += cw;
      if (cp > 0xFFFF) i++; // surrogate pair 跳过
    }
    return str;
  }
  function _visualWidth(str) {
    let w = 0;
    for (const ch of str) w += _isFullWidthCodePoint(ch.codePointAt(0)) ? 2 : 1;
    return w;
  }
  function formatFavoriteThreadMenuText(item) {
    const text = item && item.desc ? item.desc : item && item.threadId ? item.threadId : '';
    const VISUAL_MAX = 18; // 侧边栏实际可用:≈7个汉字 + "……"
    const ELLIPSIS_W = 4;  // "……" 两个全角省略号 = 4个半角宽度
    const w = _visualWidth(text);
    if (w <= VISUAL_MAX) return text;
    const truncated = _truncateByVisualWidth(text, VISUAL_MAX - ELLIPSIS_W);
    // 截断后加"……"反而比原文更宽(或差不多)→ 直接显示原文,接受轻微溢出
    if (_visualWidth(truncated) + ELLIPSIS_W >= w) return text;
    return `${truncated}……`;
  }
  function normalizeFavoriteThreads(val) {
    if (!Array.isArray(val)) return [];
    const seen = new Set();
    return val.map((item) => {
      const desc = item && typeof item.desc === 'string' && isValidDesc(item.desc.trim()) ? item.desc.trim() : '';
      const threadId = normalizeFavoriteThreadInput(item && typeof item.threadId === 'string' ? item.threadId : '');
      return { desc, threadId };
    }).filter((item) => {
      if (!isValidThreadId(item.threadId) || seen.has(item.threadId)) return false;
      seen.add(item.threadId);
      return true;
    });
  }
  function collectSubscriptionFeedsFromPanel() {
    const parsed = [];
    const seen = new Map();
    let valid = true;
    $('#subscription-feed-inputs-container .subscription-feed-row').each((idx, el) => {
      const $row = $(el);
      const desc = ($row.find('.subscription-feed-desc-input').val() || '').trim();
      const uuid = ($row.find('.subscription-feed-uuid-input').val() || '').trim();
      if (!desc && !uuid) return;
      if (!isValidDesc(desc)) { toast(`第${idx + 1}组备注过长`); valid = false; return false; }
      if (!uuid) { toast(`第${idx + 1}组未指定订阅号`); valid = false; return false; }
      if (seen.has(uuid)) {
        const first = seen.get(uuid);
        const suffix = first.desc ? `(${first.desc})` : '';
        toast(`第${idx + 1}组与第${first.index}组${suffix}订阅号重复`);
        valid = false;
        return false;
      }
      seen.set(uuid, { index: idx + 1, desc });
      parsed.push({ desc, uuid });
    });
    return valid ? parsed : null;
  }
  function collectFavoriteThreadsFromPanel() {
    const parsed = [];
    const seen = new Map();
    let valid = true;
    $('#favorite-thread-inputs-container .favorite-thread-row').each((idx, el) => {
      const $row = $(el);
      const desc = ($row.find('.favorite-thread-desc-input').val() || '').trim();
      const rawThread = ($row.find('.favorite-thread-id-input').val() || '').trim();
      const threadId = normalizeFavoriteThreadInput(rawThread);
      if (!desc && !rawThread) return;
      if (!isValidDesc(desc)) { toast(`第${idx + 1}组备注过长`); valid = false; return false; }
      if (!rawThread) { toast(`第${idx + 1}组未指定串号或链接`); valid = false; return false; }
      if (!threadId) { toast(`第${idx + 1}组存在不合法串号或链接`); valid = false; return false; }
      if (seen.has(threadId)) {
        const first = seen.get(threadId);
        const suffix = first.desc ? `(${first.desc})` : '';
        toast(`第${idx + 1}组与第${first.index}组${suffix}重复`);
        valid = false;
        return false;
      }
      seen.set(threadId, { index: idx + 1, desc });
      parsed.push({ desc, threadId });
    });
    return valid ? parsed : null;
  }
  function buildFavoriteThreadRowHtml(index, item = {}) {
    const desc = item.desc || '';
    const threadId = item.threadId || '';
    return `
      <div class="favorite-thread-row" style="position:relative;margin:10px 0 8px;">
        <span style="position:absolute;top:-9px;left:10px;display:inline-block;padding:0 6px;font-size:12px;line-height:18px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;z-index:1;">#${index}</span>
        ${buildRowDragHandleHtml()}
        <button type="button" class="favorite-thread-delete" style="position:absolute;top:-9px;right:10px;width:20px;height:20px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:1;">${XDEX_SVG_X}</button>
        <div style="display:grid;grid-template-columns:minmax(0,2fr) minmax(0,3fr);gap:8px;align-items:flex-start;border:1px solid #bfa58f;border-radius:6px;padding:12px 10px 10px 26px;background:rgba(255,255,255,0.18);box-sizing:border-box;width:100%;">
          <input class="favorite-thread-desc-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="备注(可选)" value="${Utils.escapeHTML ? Utils.escapeHTML(desc) : desc}">
          <input class="favorite-thread-id-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="串号或串链接" value="${Utils.escapeHTML ? Utils.escapeHTML(threadId) : threadId}">
        </div>
      </div>`;
  }
  // 默认订阅号读写:'' = 未设置(点“订阅EX”时每次弹窗选择)
  // 读取时校验 uuid 仍存在于订阅号列表,避免删除订阅号后残留失效默认值
  function getDefaultSubscriptionFeedUuid() {
    try {
      const state = (typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state)
        ? SettingPanel.state
        : (GM_getValue(SettingPanel.key, {}) || {});
      const uuid = String(state.defaultSubscriptionFeedUuid || '').trim();
      if (!uuid) return '';
      const feeds = (typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [];
      return feeds.some((f) => String((f && f.uuid) || '').trim() === uuid) ? uuid : '';
    } catch (e) {
      return '';
    }
  }
  // 写入默认订阅号并打时间戳;取消默认('')同样打时间戳,供跨端按“新者胜”判定
  function setDefaultSubscriptionFeedUuid(uuid) {
    try {
      const next = String(uuid || '').trim();
      const base = (typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state)
        ? SettingPanel.state
        : Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      base.defaultSubscriptionFeedUuid = next;
      base.defaultSubscriptionFeedUpdatedAt = Date.now();
      if (typeof SettingPanel !== 'undefined' && SettingPanel) SettingPanel.state = base;
      GM_setValue(SettingPanel.key, base);
      try { if (SettingPanel && typeof SettingPanel.syncInputs === 'function') SettingPanel.syncInputs(); } catch (e) {}
      return true;
    } catch (e) {
      return false;
    }
  }
  function buildSubscriptionFeedRowHtml(index, item = {}) {
    const desc = item.desc || '';
    const uuid = item.uuid || '';
    const isDefault = !!uuid && uuid === getDefaultSubscriptionFeedUuid();
    const checkedAttr = isDefault ? ' checked' : '';
    return `
      <div class="subscription-feed-row" style="position:relative;margin:10px 0 8px;">
        <span style="position:absolute;top:-9px;left:10px;display:inline-block;padding:0 6px;font-size:12px;line-height:18px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;z-index:1;">#${index}</span>
        ${buildRowDragHandleHtml()}
        <button type="button" class="subscription-feed-delete" style="position:absolute;top:-9px;right:10px;width:20px;height:20px;border:1px solid #a98f7a;border-radius:999px;background:#F0E0D6;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:1;">${XDEX_SVG_X}</button>
        <div style="display:grid;grid-template-columns:minmax(0,2fr) minmax(0,3fr);gap:8px;align-items:flex-start;border:1px solid #bfa58f;border-radius:6px;padding:12px 10px 10px 26px;background:rgba(255,255,255,0.18);box-sizing:border-box;width:100%;">
          <input class="subscription-feed-desc-input" style="width:100%;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="备注(可选)" value="${Utils.escapeHTML ? Utils.escapeHTML(desc) : desc}">
          <div style="display:flex;align-items:center;gap:8px;min-width:0;">
            <input class="subscription-feed-uuid-input" style="flex:1;min-width:0;padding:5px 8px;box-sizing:border-box;border-radius:8px;" placeholder="订阅号(任意字符串)" value="${Utils.escapeHTML ? Utils.escapeHTML(uuid) : uuid}">
            <div style="display:flex;align-items:center;gap:6px;flex:0 0 auto;white-space:nowrap;">
              <input type="checkbox" id="subscription-feed-default-${index}" class="xdex-switch subscription-feed-default" role="switch"${checkedAttr}>
              <label for="subscription-feed-default-${index}" style="font-size:12px;color:#6f5d50;user-select:none;">默认</label>
            </div>
          </div>
        </div>
      </div>`;
  }

  // ─── 浏览历史 / 发言历史 / 订阅 ───
  const THREAD_HISTORY_STORAGE_KEY = 'xdex_thread_history';
  const THREAD_HISTORY_STORE_VERSION = 1;
  // const THREAD_HISTORY_LIMIT = 500;
  const THREAD_HISTORY_LIMIT = Infinity;
  const THREAD_HISTORY_EXCERPT_LIMIT = 250;
  const THREAD_HISTORY_RECORD_RETRY_LIMIT = 10;
  const THREAD_HISTORY_RECORD_RETRY_DELAY = 500;
  const THREAD_HISTORY_SYNC_EVENT = 'xdex:thread-history-changed';
  const THREAD_HISTORY_LIVE_RENDER_DEBOUNCE_DELAY = 300;
  const THREAD_HISTORY_LIVE_RENDER_MAX_WAIT = 1500;
  const THREAD_HISTORY_REVISIT_DWELL_MS = 5000;
  const HISTORY_SEARCH_INPUT_DEBOUNCE_MS = 300;
  const POST_HISTORY_STORAGE_KEY = 'xdex_post_history';
  const POST_HISTORY_STORE_VERSION = 1;
  // const POST_HISTORY_LIMIT = 500;
  const POST_HISTORY_THREAD_LIMIT = Infinity;
  const POST_HISTORY_REPLY_LIMIT = Infinity;
  const POST_HISTORY_SYNC_EVENT = 'xdex:post-history-changed';
  // 大键只承载数据;跨标签同步只订阅几十字节的版本键。
  // 否则每次全量写都会把整份历史(MB 级)投递给每个注册了大键监听的标签页。
  const THREAD_HISTORY_REV_KEY = 'xdex_thread_history_rev';
  const POST_HISTORY_REV_KEY = 'xdex_post_history_rev';
  let historyRevSeq = 0;
  function nextHistoryRev() {
    // 同一毫秒内连续写入也必须得到不同值,否则存储层可能判定“未变化”而不发变更事件
    historyRevSeq = (historyRevSeq + 1) % 1000;
    return `${Date.now().toString(36)}-${historyRevSeq.toString(36)}`;
  }
  // 迁移桥:旧版本只写大键、不写版本键。仅当版本键尚不存在时临时监听大键,
  // 等新版本写入一次版本键后立即移除,避免长期承担整值投递。
  function bindHistoryValueChangeSync(bigKey, revKey, onChange) {
    let legacyBridgeId = null;
    const removeLegacyBridge = () => {
      if (legacyBridgeId === null) return;
      const id = legacyBridgeId;
      legacyBridgeId = null;
      try {
        if (typeof GM_removeValueChangeListener === 'function') GM_removeValueChangeListener(id);
      } catch (e) {}
    };
    if (typeof GM_addValueChangeListener !== 'function') return;
    GM_addValueChangeListener(revKey, (_key, _oldValue, _newValue, remote) => {
      removeLegacyBridge();
      onChange(remote);
    });
    // 版本键存在说明已迁移完成,不再注册大键监听——这是消除“标签页数 × MB”的关键。
    // 大键监听只作为旧标签页写数据的兼容桥,不能因一次旧写入就移除(否则会丢后续旧写入的实时同步)。
    if (GM_getValue(revKey, null) == null) {
      legacyBridgeId = GM_addValueChangeListener(bigKey, (_key, _oldValue, _newValue, remote) => {
        onChange(remote);
      });
    }
  }
  const POST_HISTORY_API_BASE = `${location.origin}/Api`;
  const POST_HISTORY_REF_API_FALLBACK_BASE = 'https://api.nmb.best/api';
  const POST_HISTORY_THREAD_API_BASE = 'https://api.nmb.best/api';
  const POST_HISTORY_GET_LAST_POST_RETRY_DELAYS = [300, 800, 1500, 2500];
  const POST_HISTORY_CONFIRM_TIMEOUT_MS = 10000;
  const POST_HISTORY_REPLIES_PER_PAGE = 19;
  const POST_HISTORY_MATCH_TIME_WINDOW_MS = 45000;
  const POST_HISTORY_FORUM_FID_MAP = Object.freeze({
    '-1': '时间线',
    '4': '综合版1',
    '98': 'DANGER/U/',
    '20': '欢乐恶搞',
    '121': '速报2',
    // 测试版不在公开 getForumList,但 showf?id=122 可用;主串仅红名可见,发言历史需靠 showf 回查
    '122': '测试',
    '17': '绘画(二创)',
    '110': '社畜(校园)',
    '19': '故事(小说)',
    '81': '都市怪谈(灵异)',
    '37': '军武',
    '30': '技术宅(代码)',
    '75': '数码(装机)',
    '118': '宠物',
    '97': '女装(时尚)',
    '106': '买买买(物品推荐)',
    '14': '动画综合',
    '12': '漫画',
    '53': '婆罗门一',
    '31': '电影/电视',
    '116': '主播管人(圈内)',
    '45': '卡牌桌游',
    '9': '特摄(布袋戏)',
    '102': '战锤',
    '39': '胶佬(手办)',
    '94': '铁道厨(车辆)',
    '6': 'VOCALOID',
    '90': '小马(美漫)',
    '5': '东方Project',
    '93': '舰娘',
    '111': '跑团',
    '57': '创作茶水间',
    '91': '规则怪谈',
    '11': '海龟汤(推理)',
    '15': '科学(干货)',
    '103': '文学(推书)',
    '35': '音乐(推歌)',
    '27': 'AI(Chatgpt)',
    '115': '摄影(cos)',
    '112': 'ROLL点',
    '2': '游戏综合',
    '3': '手游专楼',
    '25': '任天堂NS',
    '22': '腾讯游戏(LOL)',
    '23': '暴雪游戏',
    '124': 'SE(FF14)',
    '70': 'V社(DOTA)',
    '28': '怪物猎人',
    '68': '鹰角游戏',
    '47': '米哈游',
    '34': '音游打卡',
    '10': '联机(服务器发布)',
    '62': '露营',
    '113': '育儿',
    '120': '自救互助',
    '32': '料理(美食)',
    '33': '体育(健身)',
    '56': '学业打卡',
    '89': '日记(树洞)',
    '18': '值班室',
    '117': '技术支持',
    '96': '版务',
    '60': '三百人委员会'
  });
  const POST_HISTORY_FORUM_GROUP_MAP = Object.freeze({

    '-1': '综合', '4': '综合', '98': '综合', '20': '综合', '121': '综合', '122': '综合', '17': '综合', '110': '综合', '19': '综合', '81': '综合', '37': '综合', '30': '综合', '75': '综合', '118': '综合', '97': '综合', '106': '综合',
    '14': '亚文化', '12': '亚文化', '53': '亚文化', '31': '亚文化', '116': '亚文化', '45': '亚文化', '9': '亚文化', '102': '亚文化', '39': '亚文化', '94': '亚文化', '6': '亚文化', '90': '亚文化', '5': '亚文化', '93': '亚文化',
    '111': '创作', '57': '创作', '91': '创作', '11': '创作', '15': '创作', '103': '创作', '35': '创作', '27': '创作', '115': '创作', '112': '创作',
    '2': '游戏', '3': '游戏', '25': '游戏', '22': '游戏', '23': '游戏', '124': '游戏', '70': '游戏', '28': '游戏', '68': '游戏', '47': '游戏', '34': '游戏', '10': '游戏',
    '62': '生活', '113': '生活', '120': '生活', '32': '生活', '33': '生活', '56': '生活', '89': '生活',
    '18': '管理', '117': '管理', '96': '管理', '60': '管理'
  });
  const POST_HISTORY_FORUM_SEARCH_META = Object.freeze({
    '98': { rawName: 'DANGER_U', showName: 'DANGER/U/', groupName: '综合' },
    '122': { rawName: '测试', showName: '测试', groupName: '综合' },
    '17': { rawName: '绘画', showName: '绘画(二创)', groupName: '综合' },
    '110': { rawName: '社畜', showName: '社畜(校园)', groupName: '综合' },
    '19': { rawName: '故事', showName: '故事(小说)', groupName: '综合' },
    '81': { rawName: '都市怪谈', showName: '都市怪谈(灵异)', groupName: '综合' },
    '30': { rawName: '技术宅', showName: '技术宅(代码)', groupName: '综合' },
    '75': { rawName: '数码', showName: '数码(装机)', groupName: '综合' },
    '97': { rawName: '女装2', showName: '女装(时尚)', groupName: '综合' },
    '106': { rawName: '买买买', showName: '买买买(物品推荐)', groupName: '综合' },
    '31': { rawName: '影视', showName: '电影/电视', groupName: '亚文化' },
    '116': { rawName: '主播管人', showName: '主播管人(圈内)', groupName: '亚文化' },
    '9': { rawName: '特摄', showName: '特摄(布袋戏)', groupName: '亚文化' },
    '39': { rawName: '胶佬', showName: '胶佬(手办)', groupName: '亚文化' },
    '94': { rawName: '铁道厨', showName: '铁道厨(车辆)', groupName: '亚文化' },
    '90': { rawName: '小马', showName: '小马(美漫)', groupName: '亚文化' },
    '11': { rawName: '海龟汤', showName: '海龟汤(推理)', groupName: '创作' },
    '15': { rawName: '科学', showName: '科学(干货)', groupName: '创作' },
    '103': { rawName: '文学', showName: '文学(推书)', groupName: '创作' },
    '35': { rawName: '音乐', showName: '音乐(推歌)', groupName: '创作' },
    '27': { rawName: 'AI', showName: 'AI(Chatgpt)', groupName: '创作' },
    '115': { rawName: '摄影', showName: '摄影(cos)', groupName: '创作' },
    '25': { rawName: '任天堂', showName: '任天堂NS', groupName: '游戏' },
    '22': { rawName: '腾讯游戏', showName: '腾讯游戏(LOL)', groupName: '游戏' },
    '124': { rawName: 'SE', showName: 'SE(FF14)', groupName: '游戏' },
    '70': { rawName: 'V社', showName: 'V社(DOTA)', groupName: '游戏' },
    '10': { rawName: '联机', showName: '联机(服务器发布)', groupName: '游戏' },
    '32': { rawName: '料理', showName: '料理(美食)', groupName: '生活' },
    '33': { rawName: '体育', showName: '体育(健身)', groupName: '生活' },
    '89': { rawName: '日记', showName: '日记(树洞)', groupName: '生活' },
    '60': { rawName: '百脑汇', showName: '三百人委员会', groupName: '管理' }
  });
  const POST_HISTORY_TIMELINE_ID_MAP = Object.freeze({
    '1': '综合线',
    '2': '创作线',
    '3': '非创作线',
    '4': '亚文化线',
    '5': '综合2线',
    '6': '游戏线',
    '7': '生活线'
  });
  const THREAD_HISTORY_SEARCH_HELP_TEXT = '普通关键词:串号、标题、名称、饼干、正文\n高级检索:\nmode:po 只看 Po 串\nmode:normal 普通串\nhas:image 带图\nhas:gif GIF\nhas:zwsp 或 has:zerowidth 含零宽字符\nhas:sage 被 SAGE 的串\n可组合:mode:po has:image has:sage 关键词';
  const postHistoryConfirmationMap = new Map(); // 等待发串确认后跳转的 Promise 存储器 { localId -> resolver }
  const POST_HISTORY_SEARCH_HELP_TEXT = '普通关键词:发言 No、串号、板块、标题、名称、Email、正文、饼干、状态\n高级检索:\nstatus:confirmed 已确认\nstatus:pending 确认中\nstatus:failed 失败\nstatus:unconfirmed 未确认\nfid:98 指定板块 ID\nforum:综合 模糊匹配板块显示名/本名/分组名\nthread:64180270 指定串号\nid:68821620 指定发言 No\npage:203 指定页码\ncookie:abc123 指定饼干\nname:无名氏 指定名称\nemail:sage 指定 Email\nhas:image 带图\nhas:gif GIF\nhas:zwsp 或 has:zerowidth 含零宽字符\n可组合:forum:综合 has:image 关键词';
  const ZERO_WIDTH_RE = /[\u200B\u200C\u200D\uFEFF]/;
  const threadHistoryDebugState = {
    loadedAt: new Date().toISOString(),
    href: location.href,
    runtime: XDEX_RUNTIME && XDEX_RUNTIME.kind,
    storageKey: THREAD_HISTORY_STORAGE_KEY,
    lastRecord: null,
    lastRender: null,
    lastPanelModule: ''
  };
  window.__xdexThreadHistoryDebug = threadHistoryDebugState;
  let threadHistoryLiveSyncBound = false;
  let threadHistoryLiveRenderTimer = 0;
  let threadHistoryLiveRenderFirstAt = 0;
  let threadHistoryLiveRenderPendingCount = 0;
  let threadHistoryReactivationTrackingInstalled = false;
  let threadHistoryDwellTimer = 0;
  let threadHistoryVisibleSince = 0;
  let threadHistoryVisibleSessionCounted = false;
  let postHistoryLiveSyncBound = false;
  let postHistoryLiveRenderTimer = 0;
  let postHistoryLiveRenderFirstAt = 0;
  let postHistoryLiveRenderPendingCount = 0;
  let postHistoryLiveRenderDirty = false;
  let postHistoryActiveType = 'reply';
  function updateThreadHistoryDebugState(patch) {
    Object.assign(threadHistoryDebugState, patch || {});
    window.__xdexThreadHistoryDebug = threadHistoryDebugState;
    return threadHistoryDebugState;
  }
  function logThreadHistory(message, details, level = 'info') {
    const payload = Object.assign({ href: location.href }, details || {});
    updateThreadHistoryDebugState({ lastLog: { message, details: payload, at: new Date().toISOString() } });
    const logger = console[level] || console.info || console.log;
    logger.call(console, `[thread-history] ${message}`, payload);
  }
  function logThreadHistoryFlat(message, details, level = 'info') {
    const payload = Object.assign({ href: location.href }, details || {});
    const text = Object.keys(payload)
      .map(key => `${key}=${JSON.stringify(payload[key])}`)
      .join(' ');
    updateThreadHistoryDebugState({ lastFlatLog: { message, details: payload, at: new Date().toISOString() } });
    const logger = console[level] || console.info || console.log;
    logger.call(console, `[thread-history] ${message} ${text}`);
  }

  function createDefaultThreadHistoryStore() {
    return {
      version: 1,
      limit: THREAD_HISTORY_LIMIT,
      items: {},
      index: {},
      tombstones: {},
      order: []
    };
  }
  function getThreadHistoryKey(mode, threadId) {
    return `${mode}:${String(threadId || '').slice(0, 8)}`;
  }
  function normalizeThreadHistoryStore(rawStore) {
    const store = Object.assign(createDefaultThreadHistoryStore(), rawStore || {});
    store.version = THREAD_HISTORY_STORE_VERSION;
    // limit 一律以常量为准:旧版本持久化的 limit:500 不再沿用(否则 Infinity 设置永不生效);Infinity 经 JSON 序列化为 null,读回后由此行回填
    store.limit = THREAD_HISTORY_LIMIT;
    store.items = store.items && typeof store.items === 'object' ? store.items : {};
    store.index = store.index && typeof store.index === 'object' ? store.index : {};
    // 墓碑: 软删除记录(deletedAt/origin/purged/revivedAt/record)。TTL 到期降级 purged(丢快照保留压制);
    // purged 超长安全期后物理清除; revivedAt 传播标记超期一并清除
    store.tombstones = store.tombstones && typeof store.tombstones === 'object' ? store.tombstones : {};
    {
      const nowTs = Date.now();
      Object.keys(store.tombstones).forEach((tKey) => {
        const t = store.tombstones[tKey];
        if (!t || typeof t !== 'object') { delete store.tombstones[tKey]; return; }
        const deletedAt = Number(t.deletedAt) || 0;
        if (t.purged) {
          if (nowTs - deletedAt > THREAD_HISTORY_TOMBSTONE_HARD_TTL_MS) delete store.tombstones[tKey];
          return;
        }
        if (t.revivedAt) {
          if (nowTs - (Number(t.revivedAt) || 0) > THREAD_HISTORY_TOMBSTONE_TTL_MS) delete store.tombstones[tKey];
          return;
        }
        if (deletedAt && nowTs - deletedAt > THREAD_HISTORY_TOMBSTONE_TTL_MS) {
          store.tombstones[tKey] = { deletedAt, origin: t.origin || 'local', purged: true };
        }
      });
    }
    const seen = new Set();
    store.order = (Array.isArray(store.order) ? store.order : [])
      .filter(key => {
        if (!store.items[key] || seen.has(key)) return false;
        seen.add(key);
        return true;
      });
    Object.keys(store.items).forEach(key => {
      const item = store.items[key];
      const idx = store.index[key];
      // 失步自愈:指纹覆盖 searchText 与谓词来源字段,任一变更即重建;存量无 fp 的索引首次读取补齐
      if (!idx || idx.fp !== threadHistoryIndexFingerprint(item)) {
        store.index[key] = buildThreadHistoryIndexEntry(item);
      }
      // 页码一致性自愈: 修复 page=97/maxVisitedPage=97/url=?page=86/lastKnownPage=86 类自相矛盾的存量脏数据
      // (WebDAV 裸展开或过时快照钳制的产物)。不变量: 串页数只增不减 → maxVisitedPage/lastKnownPage 只允许抬升
      {
        const pageNum = Math.max(1, Number(item.page) || 1);
        const maxVp = Math.max(pageNum, Number(item.maxVisitedPage) || 0);
        const lastKp = Number(item.lastKnownPage) || 0;
        let urlPage = 0;
        if (item.url) {
          const pu = parseThreadHistoryUrl(item.url);
          urlPage = (pu && pu.page) || 0;
        }
        if (Number(item.maxVisitedPage) !== maxVp || (lastKp && lastKp < maxVp) || (urlPage && urlPage !== pageNum)) {
          item.maxVisitedPage = maxVp;
          if (!lastKp || lastKp < maxVp) item.lastKnownPage = maxVp;
          // url 永远跟随 page(最近查看页): 双向脱钩都按 page 重建
          if (!item.url || (urlPage && urlPage !== pageNum)) item.url = buildThreadHistoryPageUrl(item.mode, item.threadId, pageNum);
        }
      }
      if (!seen.has(key)) {
        seen.add(key);
        store.order.push(key);
      }
    });
    Object.keys(store.index).forEach(key => {
      if (!store.items[key]) delete store.index[key];
    });
    store.order.sort((a, b) => {
      const av = Number(store.items[a] && store.items[a].lastVisitedAt) || 0;
      const bv = Number(store.items[b] && store.items[b].lastVisitedAt) || 0;
      return bv - av;
    });
    while (store.order.length > store.limit) {
      const key = store.order.pop();
      delete store.items[key];
      delete store.index[key];
    }
    return store;
  }
  // 归一化结果记忆化:检索随输入触发,写入远少于读取;缓存由 setThreadHistoryStore 与跨标签变更监听失效
  let threadHistoryStoreCache = null;
  function invalidateThreadHistoryStoreCache() { threadHistoryStoreCache = null; }
  function getThreadHistoryStore() {
    if (threadHistoryStoreCache) return threadHistoryStoreCache;
    let store;
    try {
      store = normalizeThreadHistoryStore(GM_getValue(THREAD_HISTORY_STORAGE_KEY, null));
    } catch (e) {
      store = createDefaultThreadHistoryStore();
    }
    threadHistoryStoreCache = store;
    return store;
  }
  function setThreadHistoryStore(store) {
    const normalized = normalizeThreadHistoryStore(store);
    GM_setValue(THREAD_HISTORY_STORAGE_KEY, normalized);
    GM_setValue(THREAD_HISTORY_REV_KEY, nextHistoryRev());
    threadHistoryStoreCache = normalized;
    notifyThreadHistoryStoreChanged('local-write', false);
    return normalized;
  }
  function isThreadHistoryPanelOpen() {
    const cover = document.getElementById('sp_cover');
    const module = document.getElementById('sp_module_history');
    return !!module && module.classList.contains('active') && (!cover || getComputedStyle(cover).display !== 'none');
  }
  function scheduleThreadHistoryLiveRender(source, remote) {
    const active = isThreadHistoryPanelOpen();
    const now = Date.now();
    if (!threadHistoryLiveRenderFirstAt) threadHistoryLiveRenderFirstAt = now;
    threadHistoryLiveRenderPendingCount += 1;
    updateThreadHistoryDebugState({
      lastLiveSync: {
        source,
        remote: !!remote,
        active,
        pendingCount: threadHistoryLiveRenderPendingCount,
        firstAt: threadHistoryLiveRenderFirstAt,
        at: new Date().toISOString()
      }
    });
    if (!active) {
      if (threadHistoryLiveRenderTimer) clearTimeout(threadHistoryLiveRenderTimer);
      threadHistoryLiveRenderTimer = 0;
      threadHistoryLiveRenderFirstAt = 0;
      threadHistoryLiveRenderPendingCount = 0;
      return;
    }
    const run = () => {
      threadHistoryLiveRenderTimer = 0;
      threadHistoryLiveRenderFirstAt = 0;
      threadHistoryLiveRenderPendingCount = 0;
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => renderThreadHistoryModule());
      else renderThreadHistoryModule();
    };
    if (threadHistoryLiveRenderTimer) clearTimeout(threadHistoryLiveRenderTimer);
    const elapsed = now - threadHistoryLiveRenderFirstAt;
    const delay = elapsed >= THREAD_HISTORY_LIVE_RENDER_MAX_WAIT
      ? 0
      : Math.min(THREAD_HISTORY_LIVE_RENDER_DEBOUNCE_DELAY, THREAD_HISTORY_LIVE_RENDER_MAX_WAIT - elapsed);
    threadHistoryLiveRenderTimer = setTimeout(run, delay);
  }
  function notifyThreadHistoryStoreChanged(source, remote) {
    try {
      window.dispatchEvent(new CustomEvent(THREAD_HISTORY_SYNC_EVENT, { detail: { source, remote: !!remote, at: Date.now() } }));
    } catch (e) {}
    scheduleThreadHistoryLiveRender(source, remote);
  }
  function bindThreadHistoryLiveSync() {
    if (threadHistoryLiveSyncBound) return;
    threadHistoryLiveSyncBound = true;
    if (typeof GM_addValueChangeListener === 'function') {
      try {
        bindHistoryValueChangeSync(THREAD_HISTORY_STORAGE_KEY, THREAD_HISTORY_REV_KEY, (remote) => {
          invalidateThreadHistoryStoreCache();
          scheduleThreadHistoryLiveRender('gm-value-change', remote);
          syncFavoriteThreadsLinks();
        });
      } catch (e) {
        logThreadHistory('live sync listener failed', { error: e && e.message ? e.message : String(e) }, 'warn');
      }
    }
    window.addEventListener(THREAD_HISTORY_SYNC_EVENT, (event) => {
      const detail = event && event.detail || {};
      scheduleThreadHistoryLiveRender(detail.source || 'window-event', !!detail.remote);
    });
  }
  function createDefaultPostHistoryStore() {
    return {
      version: POST_HISTORY_STORE_VERSION,
      // limit: POST_HISTORY_LIMIT,
      items: {},
      index: {},
      tombstones: {},
      order: []
    };
  }
  function normalizePostHistoryType(type) {
    return type === 'reply' ? 'reply' : 'thread';
  }
  function normalizePostHistoryStatus(status) {
    return ['pending', 'confirmed', 'unconfirmed', 'failed'].includes(status) ? status : 'pending';
  }
  function normalizePostHistoryFid(fid) {
    const value = String(fid == null ? '' : fid).trim();
    return /^-?\d+$/.test(value) ? value : '';
  }
  function getPostHistoryForumNameByFid(fid) {
    return POST_HISTORY_FORUM_FID_MAP[normalizePostHistoryFid(fid)] || '';
  }
  function normalizeHistorySearchValue(value) {
    return String(value == null ? '' : value).trim().toLowerCase();
  }
  function getPostHistoryForumSearchText(item) {
    const fid = normalizePostHistoryFid(item && item.fid);
    const meta = POST_HISTORY_FORUM_SEARCH_META[fid] || {};
    return [
      fid,
      item && item.forumName,
      getPostHistoryForumNameByFid(fid),
      meta.rawName,
      meta.showName,
      meta.groupName || POST_HISTORY_FORUM_GROUP_MAP[fid]
    ].join(' ').toLowerCase();
  }
  function getPostHistoryPostFid(post) {
    if (!post || typeof post !== 'object') return '';
    return normalizePostHistoryFid(post.fid || post.Fid || post.forum_id || post.forumId || post.forum);
  }
  function getCurrentPostHistoryFid() {
    const path = String(location && location.pathname || '');
    const forumMatch = path.match(/^\/f\/([^/?#]+)/);
    if (!forumMatch) return '';
    let forumName = '';
    try {
      forumName = decodeURIComponent(forumMatch[1] || '');
    } catch (e) {
      forumName = forumMatch[1] || '';
    }
    const normalizedName = forumName.replace(/\s+/g, '').toLowerCase();
    return Object.keys(POST_HISTORY_FORUM_FID_MAP).find(fid => {
      const name = String(POST_HISTORY_FORUM_FID_MAP[fid] || '').replace(/\s+/g, '').toLowerCase();
      return name === normalizedName;
    }) || '';
  }
  function normalizePostHistoryText(text) {
    // 旧版(破坏性):暴力替换空白、移除标签、trim — 已禁用
    // return String(text || '')
    //   .replace(/<br\s*\/?\s*>/gi, ' ')
    //   .replace(/<[^>]+>/g, '')
    //   .replace(/&nbsp;/gi, ' ')
    //   .replace(/&gt;/gi, '>')
    //   .replace(/&lt;/gi, '<')
    //   .replace(/&amp;/gi, '&')
    //   .replace(/\r\n/g, '\n')
    //   .replace(/\r/g, '\n')
    //   .replace(/[\s\u00a0]+/g, ' ')
    //   .trim();
    // 新版:忠实保留原始内容
    return String(text || '')
      .replace(/<br\s*\/?\s*>/gi, '\n')     // <br> → 换行(不是空格)
      .replace(/&gt;/gi, '>')               // 符号转义
      .replace(/&lt;/gi, '<')
      .replace(/&amp;/gi, '&')
      .replace(/&nbsp;/gi, '\u00a0')         // &nbsp; → 不间断空格(保留语义)
      .replace(/\r\n/g, '\n')
      .replace(/\r/g, '\n');
      // 不移除 HTML 标签,保留原始结构
      // 不合并空白字符,保留原始空格/全角空格/零宽空格
      // 不 trim,保留原始首尾空白
  }
  function sanitizePostHistoryServerContentHtml(content) {
    const wrapper = document.createElement('div');
    wrapper.innerHTML = String(content || '');
    return sanitizeThreadHistoryContentHtml(wrapper);
  }
  function hashPostHistoryText(text) {
    // 旧版:直接调用 normalizePostHistoryText — 已禁用(normalizePostHistoryText 已改为忠实模式)
    // const normalized = normalizePostHistoryText(text);
    // 新版:内部独立标准化,保持 hash 计算一致性
    const normalized = String(text || '')
      .replace(/<br\s*\/?\s*>/gi, ' ')
      .replace(/<[^>]+>/g, '')
      .replace(/&nbsp;/gi, ' ')
      .replace(/&gt;/gi, '>')
      .replace(/&lt;/gi, '<')
      .replace(/&amp;/gi, '&')
      .replace(/\r\n/g, '\n')
      .replace(/\r/g, '\n')
      .replace(/[\s\u00a0]+/g, ' ')
      .trim();
    let hash = 0;
    for (let i = 0; i < normalized.length; i++) {
      hash = ((hash << 5) - hash + normalized.charCodeAt(i)) | 0;
    }
    return String(hash >>> 0);
  }
  const postHistoryDebugState = window.__xdexPostHistoryDebug = window.__xdexPostHistoryDebug || { events: [] };
  function summarizePostHistoryText(text) {
    const normalized = normalizePostHistoryText(text);
    return {
      length: normalized.length,
      hash: hashPostHistoryText(normalized),
      preview: normalized.slice(0, 40)
    };
  }
  function summarizePostHistoryCandidate(post) {
    if (!post) return null;
    const resto = String(post.resto || '0').trim();
    return {
      id: String(post.id || '').trim(),
      resto,
      type: Number(resto) === 0 ? 'thread' : 'reply',
      now: post.now || '',
      img: post.img || '',
      ext: post.ext || '',
      content: summarizePostHistoryText(post.content || '')
    };
  }
  function summarizePostHistorySnapshot(snapshot) {
    if (!snapshot) return null;
    return {
      localId: snapshot.localId || '',
      type: snapshot.type || '',
      resto: snapshot.resto || '',
      contentHash: snapshot.contentHash || '',
      submittedAt: snapshot.submittedAt || 0,
      sourceUrl: snapshot.sourceUrl || ''
    };
  }
  function logPostHistory(stage, data, level = 'log') {
    const detail = Object.assign({ stage, at: Date.now() }, data || {});
    try {
      postHistoryDebugState.events.push(detail);
      if (postHistoryDebugState.events.length > 80) postHistoryDebugState.events.shift();
      postHistoryDebugState.last = detail;
    } catch (e) {}
    const method = console[level] ? level : 'log';
    console[method]('[post-history] ' + stage, detail);
  }
  window.__xdexGetPostHistoryDebug = function getPostHistoryDebug() {
    return postHistoryDebugState;
  };
  window.__xdexClearPostHistoryDebug = function clearPostHistoryDebug() {
    postHistoryDebugState.events = [];
    postHistoryDebugState.last = null;
    return postHistoryDebugState;
  };
  // 检索索引指纹:仅拼接短标量字段,成本远低于构建 searchText(含全文);指纹一致即复用旧索引
  function postHistoryIndexFingerprint(item) {
    return [
      item.id, item.postId, item.resto, item.threadId, item.fid, item.forumName,
      item.title, item.name, item.email, item.userHash, item.status, item.type,
      item.page, item.url, item.sourceUrl, item.imageFile, item.imageImg, item.imageExt,
      item.contentHash, (item.contentText || '').length, (item.contentRaw || '').length
    ].join('\u0001');
  }
  // 发言历史检索索引:与浏览历史同构,把检索期字符串拼接与正则判定前移到写入期
  function buildPostHistoryIndexEntry(item) {
    return {
      fp: postHistoryIndexFingerprint(item),
      searchText: buildPostHistorySearchText(item),
      forumText: getPostHistoryForumSearchText(item),
      type: item.type,
      status: item.status,
      fid: item.fid,
      hasImage: !!item.imageFile,
      isGif: /\.gif(?:$|[?#])/i.test(String(item.imageFile || item.imageExt || '')),
      hasZeroWidth: ZERO_WIDTH_RE.test(String(item.contentRaw || item.contentText || ''))
    };
  }
  function normalizePostHistoryStore(rawStore) {
    const store = Object.assign(createDefaultPostHistoryStore(), rawStore || {});
    store.version = POST_HISTORY_STORE_VERSION;
    // store.limit = Number(store.limit) > 0 ? Number(store.limit) : POST_HISTORY_LIMIT;
    store.items = store.items && typeof store.items === 'object' ? store.items : {};
    // 检索索引:存量数据无此字段,首次读取即补齐(派生数据,旧版本代码忽略不影响)
    store.index = store.index && typeof store.index === 'object' ? store.index : {};
    // 墓碑: 与浏览历史同构。TTL 到期降级 purged; purged 超安全期清除; revivedAt 标记超期清除
    store.tombstones = store.tombstones && typeof store.tombstones === 'object' ? store.tombstones : {};
    {
      const nowTs = Date.now();
      Object.keys(store.tombstones).forEach((tKey) => {
        const t = store.tombstones[tKey];
        if (!t || typeof t !== 'object') { delete store.tombstones[tKey]; return; }
        const deletedAt = Number(t.deletedAt) || 0;
        if (t.purged) {
          if (nowTs - deletedAt > POST_HISTORY_TOMBSTONE_HARD_TTL_MS) delete store.tombstones[tKey];
          return;
        }
        if (t.revivedAt) {
          if (nowTs - (Number(t.revivedAt) || 0) > POST_HISTORY_TOMBSTONE_TTL_MS) delete store.tombstones[tKey];
          return;
        }
        if (deletedAt && nowTs - deletedAt > POST_HISTORY_TOMBSTONE_TTL_MS) {
          store.tombstones[tKey] = { deletedAt, origin: t.origin || 'local', purged: true };
        }
      });
    }
    const seen = new Set();
    store.order = (Array.isArray(store.order) ? store.order : [])
      .filter(key => {
        if (!store.items[key] || seen.has(key)) return false;
        seen.add(key);
        return true;
      });
    Object.keys(store.items).forEach(key => {
      const item = store.items[key] || {};
      item.localId = item.localId || key;
      item.status = normalizePostHistoryStatus(item.status);
      item.type = normalizePostHistoryType(item.type);
      item.fid = normalizePostHistoryFid(item.fid);
      item.forumName = item.forumName || getPostHistoryForumNameByFid(item.fid);
      item.contentText = normalizePostHistoryText(item.contentText || item.contentRaw || '');
      item.contentHash = item.contentHash || hashPostHistoryText(item.contentText);
      item.page = Math.max(0, Number(item.page) || 0);
      store.items[key] = item;
      const idx = store.index[key];
      if (!idx || idx.fp !== postHistoryIndexFingerprint(item)) store.index[key] = buildPostHistoryIndexEntry(item);
      if (!seen.has(key)) {
        seen.add(key);
        store.order.push(key);
      }
    });
    Object.keys(store.index).forEach(key => { if (!store.items[key]) delete store.index[key]; });
    store.order.sort((a, b) => {
      const av = Number(store.items[a] && store.items[a].submittedAt) || 0;
      const bv = Number(store.items[b] && store.items[b].submittedAt) || 0;
      return bv - av;
    });
    // 旧:全局共享 limit 清理
    // while (store.order.length > store.limit) {
    //   const key = store.order.pop();
    //   delete store.items[key];
    // }
    // 新增:按类型分别清理(仅在 limit !== Infinity 时生效)
    const shouldCleanThread = POST_HISTORY_THREAD_LIMIT !== Infinity;
    const shouldCleanReply = POST_HISTORY_REPLY_LIMIT !== Infinity;
    if (shouldCleanThread || shouldCleanReply) {
      const typeOrders = { thread: [], reply: [] };
      store.order.forEach(key => {
        const type = normalizePostHistoryType(store.items[key]?.type);
        typeOrders[type].push(key);
      });
      while (shouldCleanThread && typeOrders.thread.length > POST_HISTORY_THREAD_LIMIT) {
        const key = typeOrders.thread.pop();
        delete store.items[key];
      }
      while (shouldCleanReply && typeOrders.reply.length > POST_HISTORY_REPLY_LIMIT) {
        const key = typeOrders.reply.pop();
        delete store.items[key];
      }
      store.order = store.order.filter(key => store.items[key]);
    }
    return store;
  }
  // 同浏览历史:归一化结果记忆化,失效点在 setPostHistoryStore 与跨标签变更监听
  let postHistoryStoreCache = null;
  function invalidatePostHistoryStoreCache() { postHistoryStoreCache = null; }
  function getPostHistoryStore() {
    if (postHistoryStoreCache) return postHistoryStoreCache;
    let store;
    try {
      store = normalizePostHistoryStore(GM_getValue(POST_HISTORY_STORAGE_KEY, null));
    } catch (e) {
      store = createDefaultPostHistoryStore();
    }
    postHistoryStoreCache = store;
    return store;
  }
  function isPostHistoryPanelOpen() {
    const cover = document.getElementById('sp_cover');
    const module = document.getElementById('sp_module_posts');
    return !!module && module.classList.contains('active') && (!cover || getComputedStyle(cover).display !== 'none');
  }
  function schedulePostHistoryLiveRender(source, remote) {
    const active = isPostHistoryPanelOpen();
    const renderable = !!document.getElementById('sp_posts_results');
    const now = Date.now();
    if (!postHistoryLiveRenderFirstAt) postHistoryLiveRenderFirstAt = now;
    postHistoryLiveRenderPendingCount += 1;
    postHistoryLiveRenderDirty = true;
    logPostHistory('live sync', {
      source,
      remote: !!remote,
      active,
      renderable,
      pendingCount: postHistoryLiveRenderPendingCount,
      firstAt: postHistoryLiveRenderFirstAt
    });
    // 与浏览历史一致:模块未打开时不渲染,避免 WebDAV 合并触发全量历史渲染造成可用性检测刷屏
    if (!active) {
      if (postHistoryLiveRenderTimer) clearTimeout(postHistoryLiveRenderTimer);
      postHistoryLiveRenderTimer = 0;
      postHistoryLiveRenderFirstAt = 0;
      postHistoryLiveRenderPendingCount = 0;
      return;
    }
    const run = () => {
      postHistoryLiveRenderTimer = 0;
      postHistoryLiveRenderFirstAt = 0;
      postHistoryLiveRenderPendingCount = 0;
      postHistoryLiveRenderDirty = false;
      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => renderPostHistoryModule());
      else renderPostHistoryModule();
    };
    if (postHistoryLiveRenderTimer) clearTimeout(postHistoryLiveRenderTimer);
    const elapsed = now - postHistoryLiveRenderFirstAt;
    const delay = elapsed >= THREAD_HISTORY_LIVE_RENDER_MAX_WAIT
      ? 0
      : Math.min(THREAD_HISTORY_LIVE_RENDER_DEBOUNCE_DELAY, THREAD_HISTORY_LIVE_RENDER_MAX_WAIT - elapsed);
    postHistoryLiveRenderTimer = setTimeout(run, delay);
  }
  function notifyPostHistoryStoreChanged(source, remote) {
    try {
      window.dispatchEvent(new CustomEvent(POST_HISTORY_SYNC_EVENT, { detail: { source, remote: !!remote, at: Date.now() } }));
    } catch (e) {}
    logPostHistory('store notify', { source, remote: !!remote });
    schedulePostHistoryLiveRender(source, remote);
  }
  // ★ 发言历史持久化写合并:150ms 窗口内的多次全量写只落盘最后一次。
  // 背景:每次 GM_setValue 都触发整键序列化
  //(QwQNT 下还有 localStorage 镜像 stringify + IPC 结构化克隆 + 主进程处理),
  // 导入/WebDAV 合并路径会在同一时刻连续写 threadHistory + postHistory 等多个大键,
  // 确认链(snapshot→confirm)也会产生多次全量写。合并窗口显著削减同 tick 连环写的总量。
  // 一致性:postHistoryStoreCache 在调用方视角始终立即更新(读不受窗口影响);
  // 窗口到期才真正 GM_setValue,跨标签广播与 GM_addValueChangeListener 随真实写触发。
  const POST_HISTORY_PERSIST_DEBOUNCE_MS = 150;
  let postHistoryPersistTimer = 0;
  let postHistoryPersistPending = null;
  function flushPostHistoryStorePersist() {
    if (postHistoryPersistTimer) {
      clearTimeout(postHistoryPersistTimer);
      postHistoryPersistTimer = 0;
    }
    if (postHistoryPersistPending === null) return;
    const pending = postHistoryPersistPending;
    postHistoryPersistPending = null;
    try {
      GM_setValue(POST_HISTORY_STORAGE_KEY, pending);
      GM_setValue(POST_HISTORY_REV_KEY, nextHistoryRev());
    } catch (e) {
      console.warn('[postHistory] persist flush failed', e);
    }
  }
  function schedulePostHistoryStorePersist(normalized) {
    postHistoryPersistPending = normalized;
    if (postHistoryPersistTimer) return;
    postHistoryPersistTimer = setTimeout(() => {
      postHistoryPersistTimer = 0;
      flushPostHistoryStorePersist();
    }, POST_HISTORY_PERSIST_DEBOUNCE_MS);
  }
  // 页面关闭/隐藏前兜底落盘,避免窗口期内最后一次写丢失
  if (typeof document !== 'undefined' && !window.__xdexPostHistoryPersistFlushBound) {
    window.__xdexPostHistoryPersistFlushBound = true;
    const flushOnLeave = () => {
      // 页面隐藏/关闭:先把字段回填队列落盘(借道回填可能刚入队),再冲 persist 防抖窗口
      try { flushPostHistoryFieldPatches(); } catch (e) {}
      flushPostHistoryStorePersist();
    };
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') flushOnLeave();
    });
    // pagehide 已覆盖导航/关闭时机;不挂 beforeunload——它在导航关键路径上,
    // 同步大对象 stringify+localStorage 写会拖慢「打开新网页」的旧页面卸载
    window.addEventListener('pagehide', flushOnLeave);
  }
  // ★ 发言历史字段回填队列:thread fallback 拿到缺失字段(fid/forumName/page)后不立即
  // 全量写 store,而是入队;下一次任意 setPostHistoryStore 时免费合并,或 30s 空闲兜底
  // 一次性落盘。消除「回填 = 一次 2.9MB 全量写 + 2 轮广播」在发送后窗口内的额外卡顿。
  const postHistoryFieldPatchQueue = new Map();
  let postHistoryFieldPatchTimer = 0;
  const POST_HISTORY_FIELD_PATCH_FLUSH_MS = 1500;
  function applyPostHistoryFieldPatches(store) {
    if (!postHistoryFieldPatchQueue.size) return store;
    postHistoryFieldPatchQueue.forEach((patch, localId) => {
      const item = store.items && store.items[localId];
      if (!item) { postHistoryFieldPatchQueue.delete(localId); return; }
      Object.keys(patch).forEach(key => {
        const cur = item[key];
        const val = patch[key];
        // 只补缺失/空值,不覆盖已有数据
        if (cur === undefined || cur === null || cur === '' || cur === 0) item[key] = val;
      });
      postHistoryFieldPatchQueue.delete(localId);
    });
    return store;
  }
  function flushPostHistoryFieldPatches() {
    if (!postHistoryFieldPatchQueue.size) return;
    if (postHistoryFieldPatchTimer) { clearTimeout(postHistoryFieldPatchTimer); postHistoryFieldPatchTimer = 0; }
    const store = getPostHistoryStore();
    if (!postHistoryFieldPatchQueue.size) return;
    applyPostHistoryFieldPatches(store);
    setPostHistoryStore(store);
  }
  function schedulePostHistoryFieldPatchFlush() {
    if (postHistoryFieldPatchTimer) return;
    postHistoryFieldPatchTimer = setTimeout(() => {
      postHistoryFieldPatchTimer = 0;
      try {
        if (typeof requestIdleCallback === 'function') {
          requestIdleCallback(() => { try { flushPostHistoryFieldPatches(); } catch (e) {} }, { timeout: POST_HISTORY_FIELD_PATCH_FLUSH_MS + 3500 });
          return;
        }
      } catch (e) { /* 回退直接执行 */ }
      try { flushPostHistoryFieldPatches(); } catch (e) {}
    }, POST_HISTORY_FIELD_PATCH_FLUSH_MS);
  }
    // ★ 借道确认:发送链已拉取的 thread 数据直接回填 pending 记录的缺失字段。
  // 零新增请求、字段立即落盘(替代原 fallback 的独立 API 链与延迟写)。
  // threadObj: /api/thread 响应对象(顶层含 fid);replies: 含刚发回复的 Replies 数组;
  // page: 该回复所在页码;snapshotConstraint: 可选 { threadId } 限定串。
  function enrichPendingPostHistoryFromThreadData(threadObj, replies, page) {
    try {
      const store = getPostHistoryStore();
      const now = Date.now();
      const PENDING_TTL_MS = 180000;
      const fid = getPostHistoryPostFid(threadObj) || '';
      let enriched = 0;
      (replies || []).forEach((reply) => {
        const rid = String(reply && reply.id || '').trim();
        if (!rid) return;
        Object.keys(store.items).forEach((localId) => {
          const item = store.items[localId];
          if (!item || item.status !== 'pending' || item.type !== 'reply') return;
          if (String(item.threadId || '') !== String(threadObj && threadObj.id || '')) return;
          if (now - (Number(item.submittedAt) || 0) > PENDING_TTL_MS) return;
          if (String(item.id || '') === rid || (!item.id && postHistoryMatchesSnapshot(reply, item, new Set()))) {
            const cand = buildPostHistoryThreadCandidate(reply, threadObj, page);
            postHistoryFieldPatchQueue.set(localId, {
              fid: fid || getPostHistoryPostFid(cand) || '',
              forumName: getPostHistoryForumNameByFid(fid) || '',
              page: Math.max(1, Number(cand.page) || page || 0),
              title: String(cand.title || ''),
              name: String(cand.name || ''),
              email: String(cand.email || '')
            });
            enriched += 1;
          }
        });
      });
      if (enriched > 0) {
        logPostHistory('inline enrich', { count: enriched, page });
        schedulePostHistoryFieldPatchFlush();
      }
      return enriched;
    } catch (e) {
      logPostHistory('inline enrich error', { error: e && e.message ? e.message : String(e) }, 'warn');
      return 0;
    }
  }
  function setPostHistoryStore(store) {
    // 先把排队的字段回填合并进本次写(0 额外写)
    if (postHistoryFieldPatchQueue.size) {
      try { applyPostHistoryFieldPatches(store); } catch (e) { /* 合并失败不阻塞主写 */ }
      if (postHistoryFieldPatchTimer) { clearTimeout(postHistoryFieldPatchTimer); postHistoryFieldPatchTimer = 0; }
    }
    const normalized = normalizePostHistoryStore(store);
    postHistoryStoreCache = normalized;
    schedulePostHistoryStorePersist(normalized);
    notifyPostHistoryStoreChanged('local-write', false);
    return normalized;
  }
  function bindPostHistoryLiveSync() {
    if (postHistoryLiveSyncBound) return;
    postHistoryLiveSyncBound = true;
    if (typeof GM_addValueChangeListener === 'function') {
      try {
        bindHistoryValueChangeSync(POST_HISTORY_STORAGE_KEY, POST_HISTORY_REV_KEY, (remote) => {
          invalidatePostHistoryStoreCache();
          schedulePostHistoryLiveRender('gm-value-change', remote);
        });
      } catch (e) {
        logPostHistory('live sync listener failed', { error: e && e.message ? e.message : String(e) }, 'warn');
      }
    }
    window.addEventListener(POST_HISTORY_SYNC_EVENT, (event) => {
      const detail = event && event.detail || {};
      schedulePostHistoryLiveRender(detail.source || 'window-event', !!detail.remote);
    });
  }
  function buildCanonicalReplyUrl(threadId, replyId) {
    const tid = String(threadId || '').trim();
    const rid = String(replyId || '').trim();
    if (!tid || !rid) return '';
    return `${location.origin}/t/${tid}?r=${rid}`;
  }
  function buildPostHistoryUrl(type, id, resto) {
    const postId = String(id || '').trim();
    const threadId = String(type === 'reply' ? resto : id || '').trim();
    if (!postId && !threadId) return '';
    if (type === 'reply') return buildCanonicalReplyUrl(threadId, postId);
    // 主题也使用 ?r=threadId 格式
    // return `${location.origin}/t/${threadId}?r=${threadId}`;
    // 主题:精简版 URL,不带 ?r=threadId
    return `${location.origin}/t/${threadId}`;
  }
  function buildPostHistoryReplyActionUrl(type, id, resto, page) {
    const postId = String(id || '').trim();
    const threadId = String(type === 'reply' ? resto : id || '').trim();
    const pageNum = Math.max(0, Number(page) || 0);
    // reply 类型保持原来的行为:有 page 就用 ?page=N&r=${postId}`;,否则回退到 ?r=replyId
    if (type === 'reply') {
      if (threadId && pageNum > 0) return `${location.origin}/t/${threadId}?page=${pageNum}&r=${postId}`;
      // if (threadId && pageNum > 0) return `${location.origin}/t/${threadId}?page=${pageNum}`;
      return buildPostHistoryUrl(type, postId, threadId);
    }
    // thread 类型优先从浏览历史获取最新页面(动态更新)
    const historyUrl = getLatestThreadHistoryUrl(threadId);
    if (historyUrl) return historyUrl;
    // 没有浏览历史,回退到发言历史记录的页面
    const fallbackPage = Math.max(1, Number(page) || 1);
    if (threadId) return `${location.origin}/t/${threadId}?page=${fallbackPage}`;
    return buildPostHistoryUrl(type, postId, threadId);
  }
  function getConfirmedPostHistoryIds(store) {
    const ids = new Set();
    Object.keys(store.items || {}).forEach(key => {
      const item = store.items[key];
      if (item && item.status === 'confirmed' && item.id) ids.add(String(item.id));
    });
    return ids;
  }
  function upsertPostHistoryRecord(record) {
    if (!record || !record.localId) return getPostHistoryStore();
    const store = getPostHistoryStore();
    const old = store.items[record.localId] || {};
    const merged = Object.assign({}, old, record, {
      localId: record.localId,
      status: normalizePostHistoryStatus(record.status),
      type: normalizePostHistoryType(record.type),
      contentText: normalizePostHistoryText(record.contentText || record.contentRaw || old.contentText || ''),
      contentHash: record.contentHash || old.contentHash || hashPostHistoryText(record.contentText || record.contentRaw || old.contentText || ''),
      submittedAt: Number(record.submittedAt || old.submittedAt) || Date.now()
    });
    store.items[merged.localId] = merged;
    store.order = [merged.localId].concat((store.order || []).filter(key => key !== merged.localId));
    const typeCounts = { thread: 0, reply: 0 };
    store.order.forEach(key => {
      const type = normalizePostHistoryType(store.items[key]?.type);
      typeCounts[type]++;
    });
    logPostHistory('store upsert', {
      localId: merged.localId,
      status: merged.status,
      type: merged.type,
      contentHash: merged.contentHash,
      submittedAt: merged.submittedAt,
      total: store.order.length,
      threadCount: typeCounts.thread,
      replyCount: typeCounts.reply
    });
    return setPostHistoryStore(store);
  }
  function updatePostHistoryRecord(localId, patch) {
    const store = getPostHistoryStore();
    if (!store.items[localId]) {
      logPostHistory('store update skipped', { localId, patch: patch || {} }, 'warn');
      return store;
    }
    store.items[localId] = Object.assign({}, store.items[localId], patch || {});
    logPostHistory('store update', {
      localId,
      patch: patch || {},
      status: store.items[localId].status,
      type: store.items[localId].type,
      id: store.items[localId].id || '',
      resto: store.items[localId].resto || ''
    });
    return setPostHistoryStore(store);
  }
  function deletePostHistoryItem(localId) {
    const store = getPostHistoryStore();
    // 软删除(分家): 原记录搬入墓碑快照, 主列表移除; 30 天内可恢复
    const item = store.items[localId];
    if (item) {
      store.tombstones = store.tombstones || {};
      store.tombstones[localId] = {
        deletedAt: Date.now(),
        origin: 'local',
        purged: false,
        revivedAt: null,
        record: Object.assign({}, item)
      };
    }
    delete store.items[localId];
    store.order = (store.order || []).filter(key => key !== localId);
    return setPostHistoryStore(store);
  }
  function restorePostHistoryFromTombstone(localId) {
    const store = getPostHistoryStore();
    const tomb = (store.tombstones || {})[localId];
    if (!tomb || tomb.purged || tomb.revivedAt) return false;
    const record = tomb.record || null;
    const shadow = store.items[localId] || null;
    let merged = null;
    if (record && shadow) {
      const recNewer = (Number(record.submittedAt) || 0) >= (Number(shadow.submittedAt) || 0);
      merged = Object.assign({}, shadow, record, {
        page: Math.max(Number(record.page) || 0, Number(shadow.page) || 0),
        submittedAt: Math.min(Number(record.submittedAt) || Infinity, Number(shadow.submittedAt) || Infinity)
      });
      void recNewer;
    } else {
      merged = record ? Object.assign({}, record) : (shadow ? Object.assign({}, shadow) : null);
    }
    if (merged) {
      merged.localId = merged.localId || localId;
      store.items[localId] = merged;
      if (!(store.order || []).includes(localId)) store.order.push(localId);
    }
    store.tombstones = store.tombstones || {};
    store.tombstones[localId] = { deletedAt: Number(tomb.deletedAt) || Date.now(), origin: tomb.origin || 'local', purged: false, revivedAt: Date.now() };
    return setPostHistoryStore(store);
  }
  function purgePostHistoryTombstone(localId) {
    const store = getPostHistoryStore();
    const tomb = (store.tombstones || {})[localId];
    if (!tomb) return false;
    delete store.items[localId];
    store.order = (store.order || []).filter(key => key !== localId);
    store.tombstones[localId] = { deletedAt: Number(tomb.deletedAt) || Date.now(), origin: tomb.origin || 'local', purged: true };
    return setPostHistoryStore(store);
  }
  function purgeAllPostHistoryTombstones() {
    const store = getPostHistoryStore();
    const tombs = store.tombstones || {};
    const now = Date.now();
    Object.keys(tombs).forEach((key) => {
      if (tombs[key].purged) return;
      delete store.items[key];
      tombs[key] = { deletedAt: Number(tombs[key].deletedAt) || now, origin: tombs[key].origin || 'local', purged: true };
    });
    store.order = (store.order || []).filter(k => !tombs[k] || tombs[k].purged);
    return setPostHistoryStore(store);
  }
  function clearPostHistory() {
    // 批量软删除: 全部条目移入回收站; 30 天后自动彻底清除, 期间可恢复
    const store = getPostHistoryStore();
    const now = Date.now();
    store.tombstones = store.tombstones || {};
    Object.keys(store.items).forEach((key) => {
      if (store.tombstones[key] && !store.tombstones[key].purged && !store.tombstones[key].revivedAt) return;
      store.tombstones[key] = { deletedAt: now, origin: 'local', purged: false, revivedAt: null, record: Object.assign({}, store.items[key]) };
    });
    store.items = {};
    store.order = [];
    return setPostHistoryStore(store);
  }
  function searchPostHistory(query, type) {
    const store = getPostHistoryStore();
    const tombs = store.tombstones || {};
    const index = store.index || {};
    const selectedType = normalizePostHistoryType(type || postHistoryActiveType);
    const { filters, tokens } = parsePostHistorySearchQuery(query);
    return (store.order || [])
      .filter(key => {
        const entry = index[key];
        const item = store.items[key];
        if (!entry || !item) return false;
        // 回收站压制: 仅"无删除后新记录"的纯墓碑才隐藏; 重新添加/更新会重建 items 影子条目并带标识显示
        const tomb = tombs[key];
        if (tomb && !tomb.revivedAt && !item) return false;
        if (entry.type !== selectedType) return false;
        if (filters.statusFilters.length && !filters.statusFilters.includes(entry.status)) return false;
        if (filters.fidFilters.length && !filters.fidFilters.includes(entry.fid)) return false;
        if (filters.forumFilters.length && !filters.forumFilters.every(value => entry.forumText.includes(value))) return false;
        if (filters.hasImage && !entry.hasImage) return false;
        if (filters.isGif && !entry.isGif) return false;
        if (filters.hasZeroWidth && !entry.hasZeroWidth) return false;
        if (filters.fieldFilters.length && !filters.fieldFilters.every(filter => getPostHistorySearchFieldText(item, filter.field).includes(filter.value))) return false;
        return tokens.every(token => entry.searchText.includes(token));
      })
      .map(key => ({ key, item: store.items[key] }));
  }
  function parsePostHistorySearchQuery(query) {
    const filters = { statusFilters: [], fidFilters: [], forumFilters: [], fieldFilters: [], hasImage: false, isGif: false, hasZeroWidth: false };
    const tokens = [];
    String(query || '').split(/\s+/).filter(Boolean).forEach(rawToken => {
      const token = normalizeHistorySearchValue(rawToken);
      const pair = token.match(/^([a-z]+):(.+)$/);
      if (!pair) {
        tokens.push(token);
        return;
      }
      const key = pair[1];
      const value = normalizeHistorySearchValue(pair[2]);
      if (!value) return;
      if (key === 'status' && ['pending', 'confirmed', 'unconfirmed', 'failed'].includes(value)) filters.statusFilters.push(value);
      else if (key === 'fid') {
        const fid = normalizePostHistoryFid(value);
        if (fid) filters.fidFilters.push(fid);
      } else if (key === 'forum') filters.forumFilters.push(value);
      else if (key === 'has' && value === 'image') filters.hasImage = true;
      else if (key === 'has' && value === 'gif') filters.isGif = true;
      else if (key === 'has' && (value === 'zwsp' || value === 'zerowidth')) filters.hasZeroWidth = true;
      else if (['id', 'thread', 'page', 'cookie', 'name', 'email'].includes(key)) filters.fieldFilters.push({ field: key, value });
      else tokens.push(token);
    });
    return { filters, tokens };
  }
  function getPostHistorySearchFieldText(item, field) {
    if (field === 'id') return normalizeHistorySearchValue([item.id, item.postId].join(' '));
    if (field === 'thread') return normalizeHistorySearchValue([item.threadId, item.resto].join(' '));
    if (field === 'page') return normalizeHistorySearchValue(item.page);
    if (field === 'cookie') return normalizeHistorySearchValue(item.userHash);
    if (field === 'name') return normalizeHistorySearchValue(item.name);
    if (field === 'email') return normalizeHistorySearchValue(item.email);
    return '';
  }
  function buildPostHistorySearchText(item) {
    return [
      item.id,
      item.threadId,
      item.postId,
      item.resto,
      item.fid,
      item.forumName,
      getPostHistoryForumSearchText(item),
      item.title,
      item.name,
      item.email,
      item.contentText,
      item.contentRaw,
      item.userHash,
      item.status,
      item.type,
      item.page,
      item.url,
      item.sourceUrl,
      item.imageFile,
      item.imageImg,
      item.imageExt
    ].join(' ').toLowerCase();
  }
  function parseLastPostResponse(resp, context) {
    const text = resp && (resp.responseText || resp.response) || '';
    try {
      const json = typeof text === 'string' ? JSON.parse(text) : text;
      const data = json && (json.data || json.post || json);
      const post = Array.isArray(data) ? (data[0] || null) : data;
      if (!post) {
        logPostHistory('getLastPost empty', Object.assign({}, context || {}, { responseLength: String(text || '').length }));
        return null;
      }
      logPostHistory('getLastPost parse', Object.assign({}, context || {}, { candidate: summarizePostHistoryCandidate(post) }));
      return post;
    } catch (e) {
      logPostHistory('getLastPost parse failed', Object.assign({}, context || {}, {
        error: e && e.message ? e.message : String(e),
        responseLength: String(text || '').length,
        preview: String(text || '').slice(0, 80)
      }), 'warn');
      return null;
    }
  }
  function fetchPostHistorySameOriginText(url, context, stage) {
    const label = stage || 'post history api';
    logPostHistory(label + ' request', Object.assign({}, context || {}, { url }));
    return fetch(url, { credentials: 'include', cache: 'no-store' }).then(resp => {
      return resp.text().then(text => {
        logPostHistory(label + ' response', Object.assign({}, context || {}, {
          status: resp.status,
          responseLength: String(text || '').length
        }));
        if (!resp.ok) throw new Error(`HTTP ${resp.status} ${url}`);
        return {
          status: resp.status,
          statusText: resp.statusText,
          responseText: text,
          response: text,
          finalUrl: resp.url || url
        };
      });
    }).catch(e => {
      logPostHistory(label + ' error', Object.assign({}, context || {}, { error: e && e.message ? e.message : String(e) }), 'warn');
      throw e;
    });
  }
  function fetchLastPostHistoryPost(context) {
    const url = `${POST_HISTORY_API_BASE}/getLastPost`;
    return fetchPostHistorySameOriginText(url, context, 'getLastPost').then(resp => parseLastPostResponse(resp, context));
  }
  function getPostHistoryApiCookieHeaders() {
    const userhash = getCurrentBrowserUserhash();
    return userhash ? { Cookie: `userhash=${userhash}` } : null;
  }
  function buildPostHistoryImageFile(img, ext) {
    const base = String(img || '').trim();
    const extValue = String(ext || '').trim();
    if (!base) return '';
    const suffix = extValue ? (extValue[0] === '.' ? extValue : `.${extValue}`) : '';
    if (!suffix) return base;
    return base.toLowerCase().endsWith(suffix.toLowerCase()) ? base : base + suffix;
  }
  function parsePostHistoryRefResponse(resp, context) {
    const text = resp && (resp.responseText || resp.response) || '';
    try {
      const json = typeof text === 'string' ? JSON.parse(text) : text;
      const data = json && (json.data || json.post || json);
      const refPost = data && !Array.isArray(data) ? data : null;
      if (refPost && refPost.success === false) throw new Error(refPost.error || 'ref api error');
      if (!refPost) {
        logPostHistory('ref empty', Object.assign({}, context || {}, { responseLength: String(text || '').length }));
        return null;
      }
      logPostHistory('ref parse', Object.assign({}, context || {}, {
        id: refPost.id || '',
        imageFile: buildPostHistoryImageFile(refPost.img, refPost.ext)
      }));
      return refPost;
    } catch (e) {
      logPostHistory('ref parse failed', Object.assign({}, context || {}, {
        error: e && e.message ? e.message : String(e),
        responseLength: String(text || '').length,
        preview: String(text || '').slice(0, 80)
      }), 'warn');
      return null;
    }
  }
  function postHistoryRefPostHasImage(refPost) {
    return !!buildPostHistoryImageFile(refPost && refPost.img, refPost && refPost.ext);
  }
  function parsePostHistoryRefHtmlResponse(resp, context) {
    const html = resp && (resp.responseText || resp.response) || '';
    try {
      const doc = new DOMParser().parseFromString(String(html || ''), 'text/html');
      const root = doc.querySelector('.h-threads-img-box') || doc.querySelector('.h-threads-item-main') || doc.body;
      let imageFile = extractThreadHistoryImageFile(root);
      if (!imageFile) {
        const imageNode = doc.querySelector('.h-threads-img-a[href], img.h-threads-image, img.h-threads-img, a[href*="/image/"], a[href*="/thumb/"]');
        const imageAnchor = imageNode && imageNode.closest ? imageNode.closest('a[href]') : imageNode;
        imageFile = normalizeThreadHistoryImageFile(
          (imageAnchor && imageAnchor.getAttribute && imageAnchor.getAttribute('href')) ||
          (imageNode && imageNode.getAttribute && (imageNode.getAttribute('data-src') || imageNode.getAttribute('src')))
        );
      }
      if (!imageFile) {
        logPostHistory('ref html empty', Object.assign({}, context || {}, { responseLength: String(html || '').length }));
        return null;
      }
      logPostHistory('ref html parse', Object.assign({}, context || {}, { imageFile }));
      return { img: imageFile, ext: '', imageFile };
    } catch (e) {
      logPostHistory('ref html parse failed', Object.assign({}, context || {}, {
        error: e && e.message ? e.message : String(e),
        responseLength: String(html || '').length,
        preview: String(html || '').slice(0, 80)
      }), 'warn');
      return null;
    }
  }
  function fetchPostHistoryRefPost(id, context) {
    const postId = String(id || '').trim();
    if (!postId) return Promise.resolve(null);
    const detail = Object.assign({}, context || {}, { id: postId });
    // [停用-注释保留] 同源降级链(/Api/ref → /Home/Forum/ref)经实证(2026-08-18)对浏览器 UA 稳定返回 503/空,仅走主 API api.nmb.best/api/ref
    return fetchPostHistoryRefApiPost(postId, detail);
    // return fetchPostHistoryRefApiPost(postId, detail)
    //   .then(refPost => {
    //     if (postHistoryRefPostHasImage(refPost)) return refPost;
    //     return fetchPostHistorySameOriginRefPost(postId, detail);
    //   })
    //   .catch(() => fetchPostHistorySameOriginRefPost(postId, detail));
  }
  function fetchPostHistoryRefApiPost(id, context) {
    const postId = String(id || '').trim();
    if (!postId) return Promise.resolve(null);
    const url = `${POST_HISTORY_REF_API_FALLBACK_BASE}/ref?id=${encodeURIComponent(postId)}`;
    const headers = getPostHistoryApiCookieHeaders();
    const detail = Object.assign({}, context || {}, { id: postId, api: true, authenticated: !!headers });
    logPostHistory('ref api request', Object.assign({}, detail, { url }));
    return gmRequest(url, 'text', headers).then(resp => {
      logPostHistory('ref api response', Object.assign({}, detail, {
        status: resp.status,
        responseLength: String(resp.responseText || resp.response || '').length
      }));
      const refPost = parsePostHistoryRefResponse(resp, detail);
      return postHistoryRefPostHasImage(refPost) ? refPost : null;
    }).catch(e => {
      logPostHistory('ref api error', Object.assign({}, detail, { error: e && e.message ? e.message : String(e) }), 'warn');
      throw e;
    });
  }
  // [停用-注释保留] 同源降级(/Api/ref):经实证(2026-08-18)该端点对浏览器 UA 稳定返回 503,恢复时取消注释并恢复 fetchPostHistoryRefPost 内调用即可
  // function fetchPostHistorySameOriginRefPost(id, context) {
  //   const postId = String(id || '').trim();
  //   if (!postId) return Promise.resolve(null);
  //   const url = `${POST_HISTORY_API_BASE}/ref?id=${encodeURIComponent(postId)}`;
  //   const detail = Object.assign({}, context || {}, { id: postId, sameOriginFallback: true });
  //   return fetchPostHistorySameOriginText(url, detail, 'ref same-origin fallback')
  //     .then(resp => {
  //       const refPost = parsePostHistoryRefResponse(resp, detail);
  //       if (postHistoryRefPostHasImage(refPost)) return refPost;
  //       return fetchPostHistoryRefHtmlFallbackPost(postId, detail);
  //     })
  //     .catch(() => fetchPostHistoryRefHtmlFallbackPost(postId, detail));
  // }
  // [停用-注释保留] HTML 降级(/Home/Forum/ref):实证返回空页,随 /Api/ref 降级一并停用
  // function fetchPostHistoryRefHtmlFallbackPost(id, context) {
  //   const postId = String(id || '').trim();
  //   if (!postId) return Promise.resolve(null);
  //   const url = `/Home/Forum/ref?id=${encodeURIComponent(postId)}`;
  //   const detail = Object.assign({}, context || {}, { id: postId, htmlFallback: true });
  //   return fetchPostHistorySameOriginText(url, detail, 'ref html fallback')
  //     .then(resp => parsePostHistoryRefHtmlResponse(resp, detail));
  // }
  function enrichPostHistoryRefImage(localId, postId) {
    return fetchPostHistoryRefPost(postId, { localId }).then(refPost => {
      const imageFile = refPost ? buildPostHistoryImageFile(refPost.img, refPost.ext) : '';
      if (!imageFile) return false;
      updatePostHistoryRecord(localId, { imageFile, imageImg: refPost.img || '', imageExt: refPost.ext || '' });
      return true;
    }).catch(e => {
      logPostHistory('ref image error', { localId, id: postId, error: e && e.message ? e.message : String(e) }, 'warn');
      return false;
    });
  }
  function parsePostHistoryThreadResponse(resp, context) {
    const text = resp && (resp.responseText || resp.response) || '';
    try {
      const thread = typeof text === 'string' ? JSON.parse(text) : text;
      if (thread && thread.success === false) throw new Error(thread.error || 'thread api error');
      const replies = Array.isArray(thread && thread.Replies) ? thread.Replies : [];
      const replyCount = Number(thread && (thread.ReplyCount || thread.replyCount || thread.reply_count)) || replies.length;
      logPostHistory('thread fallback parse', Object.assign({}, context || {}, { replyCount, replies: replies.length }));
      return { thread, replies, replyCount, page: Math.max(1, Number(context && context.page) || 1) };
    } catch (e) {
      logPostHistory('thread fallback parse failed', Object.assign({}, context || {}, {
        error: e && e.message ? e.message : String(e),
        responseLength: String(text || '').length,
        preview: String(text || '').slice(0, 80)
      }), 'warn');
      throw e;
    }
  }
  function fetchPostHistoryThreadPage(threadId, page, context) {
    const detail = Object.assign({}, context || {}, { threadId, page });
    // [停用-注释保留] 同源降级(/Api/thread)经实证(2026-08-18)对浏览器 UA 稳定返回 503,仅走主 API api.nmb.best/api/thread
    return fetchPostHistoryThreadApiPage(threadId, page, detail);
    // return fetchPostHistoryThreadApiPage(threadId, page, detail)
    //   .catch(() => fetchPostHistorySameOriginThreadPage(threadId, page, detail));
  }
  function fetchPostHistoryThreadApiPage(threadId, page, context) {
    const url = `${POST_HISTORY_THREAD_API_BASE}/thread?id=${encodeURIComponent(threadId)}&page=${encodeURIComponent(page)}`;
    const headers = getPostHistoryApiCookieHeaders();
    const detail = Object.assign({}, context || {}, { threadId, page, api: true, authenticated: !!headers });
    logPostHistory('thread api request', Object.assign({}, detail, { url }));
    return gmRequest(url, 'text', headers).then(resp => {
      logPostHistory('thread api response', Object.assign({}, detail, {
        status: resp.status,
        responseLength: String(resp.responseText || resp.response || '').length
      }));
      return parsePostHistoryThreadResponse(resp, detail);
    }).catch(e => {
      logPostHistory('thread api error', Object.assign({}, detail, { error: e && e.message ? e.message : String(e) }), 'warn');
      throw e;
    });
  }
  // [停用-注释保留] 同源降级(/Api/thread):经实证(2026-08-18)该端点对浏览器 UA 稳定返回 503,恢复时取消注释并恢复 fetchPostHistoryThreadPage 内的 catch 即可
  // function fetchPostHistorySameOriginThreadPage(threadId, page, context) {
  //   const url = `${POST_HISTORY_API_BASE}/thread?id=${encodeURIComponent(threadId)}&page=${encodeURIComponent(page)}`;
  //   const detail = Object.assign({}, context || {}, { threadId, page });
  //   return fetchPostHistorySameOriginText(url, detail, 'thread same-origin fallback').then(resp => parsePostHistoryThreadResponse(resp, detail));
  // }
  function getPostHistoryThreadFallbackPages(replyCount) {
    const total = Number(replyCount) || 0;
    const tailPage = Math.max(1, Math.ceil(total / POST_HISTORY_REPLIES_PER_PAGE));
    const pages = [tailPage, tailPage - 1]
      .filter(page => page >= 1)
      .map(page => Math.max(1, Number(page) || 1));
    return Array.from(new Set(pages)).sort((a, b) => b - a);
  }
  function buildPostHistoryThreadCandidate(reply, thread, page) {
    const threadId = String(thread && thread.id || '').trim();
    const fid = getPostHistoryPostFid(reply) || getPostHistoryPostFid(thread);
    return Object.assign({}, reply || {}, { fid, resto: threadId, page: Math.max(1, Number(page) || 1) });
  }
  function findPostHistoryThreadFallbackMatch(pageData, snapshot, usedIds) {
    const replies = Array.isArray(pageData && pageData.replies) ? pageData.replies : [];
    for (let i = replies.length - 1; i >= 0; i--) {
      const candidate = buildPostHistoryThreadCandidate(replies[i], pageData && pageData.thread || { id: snapshot && snapshot.resto, fid: snapshot && snapshot.fid }, pageData && pageData.page);
      if (postHistoryMatchesSnapshot(candidate, snapshot, usedIds)) return candidate;
    }
    return null;
  }
  const POST_HISTORY_THREAD_PAGE_RETRY_DELAYS = [5000, 15000, 30000, 60000, 120000];
  // 主路径确认后的 thread 尾页回退调度:错峰到空闲时段,避免发送后的存储写敏感窗口。
  // 只调度一次(pending 标记防重复);页面在此期间关闭则放弃(字段缺口无碍主数据)。
  function schedulePostHistoryThreadFallback(localId, snapshot) {
    const run = () => { try { completePostHistoryFromThreadFallback(localId, snapshot, 0); } catch (e) { logPostHistory('thread fallback schedule error', { localId, error: e && e.message ? e.message : String(e) }, 'warn'); } };
    const IDLE_DELAY_MS = 5000;
    try {
      if (typeof requestIdleCallback === 'function') {
        requestIdleCallback(() => setTimeout(run, 0), { timeout: IDLE_DELAY_MS + 3000 });
        return;
      }
    } catch (e) { /* 回退 setTimeout */ }
    setTimeout(run, IDLE_DELAY_MS);
  }
  async function completePostHistoryFromThreadFallback(localId, snapshot, retryAttempt) {
    if (!snapshot || snapshot.type !== 'reply' || !String(snapshot.resto || '').trim()) {
      logPostHistory('thread fallback exhausted', { localId, reason: 'unsupported-snapshot', snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
      return false;
    }
    const threadId = String(snapshot.resto || '').trim();
    try {
      const firstPage = await fetchPostHistoryThreadPage(threadId, 1, { localId, phase: 'count', retryAttempt: retryAttempt || 0 });
      const pages = getPostHistoryThreadFallbackPages(firstPage.replyCount);
      const usedIds = getConfirmedPostHistoryIds(getPostHistoryStore());
      for (const page of pages) {
        const pageData = page === 1 ? firstPage : await fetchPostHistoryThreadPage(threadId, page, { localId, phase: 'scan' });
        const post = findPostHistoryThreadFallbackMatch(pageData, snapshot, usedIds);
        if (post) {
          logPostHistory('thread fallback confirmed', { localId, page, retryAttempt: retryAttempt || 0, candidate: summarizePostHistoryCandidate(post) });
          // ★ 只回填主路径拿不到的字段(fid/forumName/page/title 等),入队等待合并写:
          // 不再走 confirmPostHistorySnapshot(那会整包 update + 再次状态翻转 = 又一次
          // 2.9MB 全量存储写 + 2 轮广播,实测是发送后卡顿的残留源)。条目状态仍为
          // 主路径已写的 confirmed,无字段翻转风险。
          // 借道回填通常已补齐字段(fid 非空)→ 无缺口则跳过,避免一次空的全量写
          const existingItem = getPostHistoryStore().items[localId];
          const needsBackfill = !existingItem || !existingItem.fid || !existingItem.page;
          if (needsBackfill) {
            postHistoryFieldPatchQueue.set(localId, {
              fid: getPostHistoryPostFid(post) || '',
              forumName: getPostHistoryForumNameByFid(getPostHistoryPostFid(post)) || '',
              page: Math.max(1, Number(post.page) || 0),
              title: String(post.title || ''),
              name: String(post.name || ''),
              email: String(post.email || '')
            });
            schedulePostHistoryFieldPatchFlush();
          }
          const resolver = postHistoryConfirmationMap.get(localId);
          if (resolver) { try { resolver({ localId, fromFallback: true }); postHistoryConfirmationMap.delete(localId); } catch (e) {} }
          return true;
        }
      }
      logPostHistory('thread fallback exhausted', { localId, pages, snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
      return false;
    } catch (e) {
      const attempt = retryAttempt || 0;
      if (attempt < POST_HISTORY_THREAD_PAGE_RETRY_DELAYS.length) {
        const delay = POST_HISTORY_THREAD_PAGE_RETRY_DELAYS[attempt];
        logPostHistory('thread page verify retry scheduled', { localId, threadId, attempt, nextAttempt: attempt + 1, delay, error: e && e.message ? e.message : String(e) });
        setTimeout(() => {
          completePostHistoryFromThreadFallback(localId, snapshot, attempt + 1);
        }, delay);
        return false;
      }
      logPostHistory('thread page verify error', { localId, threadId, attempts: attempt + 1, error: e && e.message ? e.message : String(e) }, 'warn');
      return false;
    }
  }
  // showf 版面主串回退:新开串在 getLastPost 失败、且 thread fallback 不适用时,按 fid 扫 /showf
  // 典型场景:/f/测试(fid=122)主串不进时间线、对普通浏览半隐身,但 showf?id=122 可回查刚发主串
  const POST_HISTORY_SHOWF_PAGE_RETRY_DELAYS = [5000, 15000, 30000, 60000, 120000];
  function parsePostHistoryShowfResponse(resp, context) {
    const text = resp && (resp.responseText || resp.response) || '';
    try {
      const json = typeof text === 'string' ? JSON.parse(text) : text;
      if (json && json.success === false) throw new Error(json.error || 'showf api error');
      const posts = Array.isArray(json) ? json : (Array.isArray(json && json.data) ? json.data : []);
      logPostHistory('showf fallback parse', Object.assign({}, context || {}, { posts: posts.length }));
      return { posts, page: Math.max(1, Number(context && context.page) || 1) };
    } catch (e) {
      logPostHistory('showf fallback parse failed', Object.assign({}, context || {}, {
        error: e && e.message ? e.message : String(e),
        responseLength: String(text || '').length,
        preview: String(text || '').slice(0, 80)
      }), 'warn');
      throw e;
    }
  }
  function fetchPostHistoryShowfPage(fid, page, context) {
    const detail = Object.assign({}, context || {}, { fid, page });
    return fetchPostHistoryShowfApiPage(fid, page, detail)
      .catch(() => fetchPostHistorySameOriginShowfPage(fid, page, detail));
  }
  function fetchPostHistoryShowfApiPage(fid, page, context) {
    const url = `${POST_HISTORY_SHOWF_API_BASE}/showf?id=${encodeURIComponent(fid)}&page=${encodeURIComponent(page)}`;
    const headers = getPostHistoryApiCookieHeaders();
    const detail = Object.assign({}, context || {}, { fid, page, api: true, authenticated: !!headers });
    logPostHistory('showf api request', Object.assign({}, detail, { url }));
    return gmRequest(url, 'text', headers).then(resp => {
      logPostHistory('showf api response', Object.assign({}, detail, {
        status: resp.status,
        responseLength: String(resp.responseText || resp.response || '').length
      }));
      return parsePostHistoryShowfResponse(resp, detail);
    }).catch(e => {
      logPostHistory('showf api error', Object.assign({}, detail, { error: e && e.message ? e.message : String(e) }), 'warn');
      throw e;
    });
  }
  function fetchPostHistorySameOriginShowfPage(fid, page, context) {
    const url = `${POST_HISTORY_API_BASE}/showf?id=${encodeURIComponent(fid)}&page=${encodeURIComponent(page)}`;
    const detail = Object.assign({}, context || {}, { fid, page });
    return fetchPostHistorySameOriginText(url, detail, 'showf same-origin fallback').then(resp => parsePostHistoryShowfResponse(resp, detail));
  }
  function buildPostHistoryShowfCandidate(post, page, snapshotFid) {
    const fid = getPostHistoryPostFid(post) || normalizePostHistoryFid(snapshotFid);
    // showf 列表项是主串;resto 缺省时按 0 处理,便于 postHistoryMatchesSnapshot 归类为 thread
    return Object.assign({}, post || {}, {
      fid,
      resto: String(post && post.resto != null ? post.resto : '0'),
      page: Math.max(1, Number(page) || 1)
    });
  }
  function findPostHistoryShowfFallbackMatch(pageData, snapshot, usedIds) {
    const posts = Array.isArray(pageData && pageData.posts) ? pageData.posts : [];
    // 版面列表通常新串在前;从前往后扫即可,匹配逻辑仍靠内容/时间窗
    for (let i = 0; i < posts.length; i++) {
      const candidate = buildPostHistoryShowfCandidate(posts[i], pageData && pageData.page, snapshot && snapshot.fid);
      if (postHistoryMatchesSnapshot(candidate, snapshot, usedIds)) return candidate;
    }
    return null;
  }
  async function completePostHistoryFromShowfFallback(localId, snapshot, retryAttempt) {
    if (!snapshot || snapshot.type !== 'thread') {
      logPostHistory('showf fallback exhausted', { localId, reason: 'unsupported-snapshot', snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
      return false;
    }
    const fid = normalizePostHistoryFid(snapshot.fid) || getCurrentPostHistoryFid();
    if (!fid) {
      logPostHistory('showf fallback exhausted', { localId, reason: 'missing-fid', snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
      return false;
    }
    try {
      // 只扫前两页:新开串通常在 page=1;page=2 覆盖并发/排序抖动
      const pages = [1, 2];
      const usedIds = getConfirmedPostHistoryIds(getPostHistoryStore());
      for (const page of pages) {
        const pageData = await fetchPostHistoryShowfPage(fid, page, { localId, phase: 'scan', retryAttempt: retryAttempt || 0 });
        const post = findPostHistoryShowfFallbackMatch(pageData, Object.assign({}, snapshot, { fid }), usedIds);
        if (post) {
          logPostHistory('showf fallback confirmed', { localId, fid, page, retryAttempt: retryAttempt || 0, candidate: summarizePostHistoryCandidate(post) });
          confirmPostHistorySnapshot(localId, post);
          return true;
        }
      }
      logPostHistory('showf fallback exhausted', { localId, fid, pages, snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
      return false;
    } catch (e) {
      const attempt = retryAttempt || 0;
      if (attempt < POST_HISTORY_SHOWF_PAGE_RETRY_DELAYS.length) {
        const delay = POST_HISTORY_SHOWF_PAGE_RETRY_DELAYS[attempt];
        logPostHistory('showf page verify retry scheduled', { localId, fid, attempt, nextAttempt: attempt + 1, delay, error: e && e.message ? e.message : String(e) });
        setTimeout(() => {
          completePostHistoryFromShowfFallback(localId, snapshot, attempt + 1);
        }, delay);
        return false;
      }
      logPostHistory('showf page verify error', { localId, fid, attempts: attempt + 1, error: e && e.message ? e.message : String(e) }, 'warn');
      return false;
    }
  }
  function postHistoryMatchesSnapshot(post, snapshot, usedIds) {
    const reject = (reason, extra) => {
      logPostHistory('match rejected', Object.assign({
        reason,
        snapshot: summarizePostHistorySnapshot(snapshot),
        candidate: summarizePostHistoryCandidate(post)
      }, extra || {}));
      return false;
    };
    if (!post || !snapshot) return reject('missing-post-or-snapshot');
    const id = String(post.id || '').trim();
    if (!id) return reject('missing-id');
    const expectedId = String(snapshot.id || snapshot.postId || '').trim();
    if (expectedId && id !== expectedId) return reject('id-mismatch', { expectedId, actualId: id });
    if (usedIds.has(id) && id !== expectedId) return reject('duplicate-confirmed-id', { id });
    const resto = String(post.resto || '0').trim();
    const type = Number(resto) === 0 ? 'thread' : 'reply';
    if (type !== snapshot.type) return reject('type-mismatch', { expectedType: snapshot.type, actualType: type });
    if (type === 'reply' && String(snapshot.resto || '').trim() && String(snapshot.resto || '').trim() !== resto) return reject('reply-resto-mismatch', { expectedResto: String(snapshot.resto || '').trim(), actualResto: resto });
    const postText = normalizePostHistoryText(post.content || '');
    if (postText && snapshot.contentHash && hashPostHistoryText(postText) !== snapshot.contentHash && postText !== snapshot.contentText) {
      logPostHistory('server content differs', {
        snapshot: summarizePostHistorySnapshot(snapshot),
        candidate: summarizePostHistoryCandidate(post),
        expectedHash: snapshot.contentHash,
        actualHash: hashPostHistoryText(postText)
      });
    }
    const postTs = Date.parse(post.now || '');
    if (!postTs) return reject('missing-time');
    const timeDiff = Math.abs(postTs - Number(snapshot.submittedAt || Date.now()));
    if (timeDiff > POST_HISTORY_MATCH_TIME_WINDOW_MS) return reject('time-window-mismatch', { postTs, submittedAt: Number(snapshot.submittedAt || Date.now()), timeDiff });
    logPostHistory('match accepted', {
      snapshot: summarizePostHistorySnapshot(snapshot),
      candidate: summarizePostHistoryCandidate(post),
      timeDiff
    });
    return true;
  }
  function confirmPostHistorySnapshot(localId, post) {
    const resto = String(post.resto || '0').trim();
    const type = Number(resto) === 0 ? 'thread' : 'reply';
    const id = String(post.id || '').trim();
    const url = buildPostHistoryUrl(type, id, resto);
    const existing = getPostHistoryStore().items[localId] || {};
    const existingPage = type === 'thread' ? (Number(existing.page) || 0) : 0;
    const imageFile = buildPostHistoryImageFile(post.img, post.ext);
    const serverContentRaw = post.content || '';
    const serverContentText = normalizePostHistoryText(serverContentRaw);
    const fid = getPostHistoryPostFid(post) || normalizePostHistoryFid(existing.fid);
    const update = {
      status: 'confirmed',
      type,
      id,
      resto,
      threadId: type === 'reply' ? resto : id,
      postId: id,
      page: Math.max(0, Number(post.page) || existingPage || (type === 'thread' ? 1 : 0)),
      fid,
      forumName: getPostHistoryForumNameByFid(fid),
      title: post.title || '',
      email: post.email || '',
      contentRaw: serverContentRaw,
      contentText: serverContentText,
      contentHash: hashPostHistoryText(serverContentText),
      contentHtml: sanitizePostHistoryServerContentHtml(serverContentRaw),
      userHash: post.user_hash || post.userHash || '',
      confirmedAt: Date.now(),
      url
    };
    if (imageFile) Object.assign(update, { imageFile, imageImg: post.img || '', imageExt: post.ext || '' });
    logPostHistory('confirmed', { localId, type, id, resto, url });
    updatePostHistoryRecord(localId, update);
    // 新串发布成功 → 自动写入串内饼干偏好
    if (type === 'thread' && update.userHash) {
      try { setThreadCookiePref(id, update.userHash); if (typeof refreshCookiePrefSwitchState === 'function') refreshCookiePrefSwitchState(); } catch (e) {}
    }
    const resolver = postHistoryConfirmationMap.get(localId);
    if (!imageFile) {
      // 没有图片,异步获取后再通知等待者
      enrichPostHistoryRefImage(localId, id).then(() => {
        if (resolver) {
          resolver(Object.assign({ localId }, update));
          postHistoryConfirmationMap.delete(localId);
        }
      }).catch(() => {
        if (resolver) {
          resolver(Object.assign({ localId }, update));
          postHistoryConfirmationMap.delete(localId);
        }
      });
    } else {
      // 已有图片,直接通知等待者
      if (resolver) {
        resolver(Object.assign({ localId }, update));
        postHistoryConfirmationMap.delete(localId);
      }
    }
  }
  function completePostHistorySnapshot(localId, snapshot, attempt = 0) {
    const delay = POST_HISTORY_GET_LAST_POST_RETRY_DELAYS[attempt];
    if (delay == null) {
      logPostHistory('completion exhausted', { localId, attempt, snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
      // 清理等待者(确认失败或超时)
      const resolver = postHistoryConfirmationMap.get(localId);
      if (resolver) {
        resolver(null);
        postHistoryConfirmationMap.delete(localId);
      }
      // reply → thread 尾页回退;thread(含测试版主串)→ showf 版面回退
      completePostHistoryFromThreadFallback(localId, snapshot).then(confirmed => {
        if (confirmed) return true;
        return completePostHistoryFromShowfFallback(localId, snapshot);
      }).then(confirmed => {
        if (confirmed) return;
        logPostHistory('unconfirmed', { localId, attempt, snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
        updatePostHistoryRecord(localId, { status: 'unconfirmed' });
      }).catch(e => {
        logPostHistory('thread fallback error', { localId, error: e && e.message ? e.message : String(e) }, 'warn');
        // thread 路径异常时仍尝试 showf(新开串场景)
        completePostHistoryFromShowfFallback(localId, snapshot).then(confirmed => {
          if (confirmed) return;
          logPostHistory('unconfirmed', { localId, attempt, snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
          updatePostHistoryRecord(localId, { status: 'unconfirmed' });
        }).catch(showfErr => {
          logPostHistory('showf fallback error', { localId, error: showfErr && showfErr.message ? showfErr.message : String(showfErr) }, 'warn');
          logPostHistory('unconfirmed', { localId, attempt, snapshot: summarizePostHistorySnapshot(snapshot) }, 'warn');
          updatePostHistoryRecord(localId, { status: 'unconfirmed' });
        });
      });
      return;
    }
    logPostHistory('completion scheduled', { localId, attempt, delay, snapshot: summarizePostHistorySnapshot(snapshot) });
    setTimeout(() => {
      fetchLastPostHistoryPost({ localId, attempt }).then(post => {
        const store = getPostHistoryStore();
        if (!postHistoryMatchesSnapshot(post, snapshot, getConfirmedPostHistoryIds(store))) {
          logPostHistory('completion retry', { localId, attempt, nextAttempt: attempt + 1 });
          completePostHistorySnapshot(localId, snapshot, attempt + 1);
          return;
        }
        const id = String(post.id || '').trim();
        const confirmedResto = String(post.resto || snapshot.resto || '').trim();
        const confirmedSnapshot = Object.assign({}, snapshot, { id, postId: id, resto: confirmedResto, threadId: confirmedResto });
        confirmPostHistorySnapshot(localId, post);
        // ★ 主路径确认后仍需 thread 尾页回退:getLastPost 不返回 fid/forumName/page/title
        //   等字段(实测历史条目缺「技术支持/所在页」等),fallback 的 thread 页 candidate
        //   负责补齐。但它会再次 confirm → 一次全量存储写 + 2 轮跨标签广播(每次广播
        //   渲染端 valuesEqual 双 stringify 大对象)——错峰到空闲时段执行,避免叠加在
        //   发送后 0-3s 的滚动敏感窗口(实测该窗口内 3 次全量写 = 持续卡顿)。
        if (confirmedSnapshot.type === 'reply') {
          schedulePostHistoryThreadFallback(localId, confirmedSnapshot);
        }
      }).catch(e => {
        logPostHistory('completion retry', { localId, attempt, nextAttempt: attempt + 1, error: e && e.message ? e.message : String(e) }, 'warn');
        completePostHistorySnapshot(localId, snapshot, attempt + 1);
      });
    }, delay);
  }
  // ★ 轻量确认(发言历史关闭时专用):只通过 getLastPost 重试链取回新串地址用于跳转,
// 完全不写发言历史 store(无 upsert/update/广播)。
  function confirmPostHistoryUrlOnly(fd, options) {
    const type = options && options.isReply ? 'reply' : 'thread';
    const contentRaw = fd && fd.get ? String(fd.get('content') || '') : '';
    const contentText = normalizePostHistoryText(contentRaw);
    const contentHash = hashPostHistoryText(contentText);
    const resto = fd && fd.get ? String(fd.get('resto') || '').trim() : '';
    const title = fd && fd.get ? String(fd.get('title') || '') : '';
    const probes = {
      status: 'pending', type, localId: '', id: '', postId: '', resto,
      threadId: type === 'reply' ? resto : '',
      title, contentText, contentHash, userHash: '',
      submittedAt: Date.now()
    };
    const delays = POST_HISTORY_GET_LAST_POST_RETRY_DELAYS;
    return new Promise((resolve) => {
      const attemptFetch = (attempt) => {
        const delay = delays[attempt];
        if (delay == null) { resolve(null); return; }
        setTimeout(() => {
          fetchLastPostHistoryPost({ localId: 'url-only', attempt }).then(post => {
            // 复用主链同源匹配判据(null 安全;usedIds 用空集——只确认不落库)
            if (!postHistoryMatchesSnapshot(post, probes, new Set())) {
              attemptFetch(attempt + 1);
              return;
            }
            const id = String(post.id || '').trim();
            resolve(id ? { url: buildPostHistoryUrl(type, id, String(post.resto || resto || '').trim()), id } : null);
          }).catch(() => { attemptFetch(attempt + 1); });
        }, delay);
      };
      attemptFetch(0);
    });
  }
  function snapshotSubmittedPostHistory(fd, options) {
    // 发言历史已关闭:不在发送消息后自动记录。但发串跳转(postAfterAction=jump/refresh)
    // 依赖确认后的地址——走"只确认不落库"的轻量路径(getLastPost 重试取 id,不写 store)。
    if (!isPostHistoryEnabledNow()) {
      const isReplyDisabled = !!(options && options.isReply);
      return {
        snapshot: null,
        localId: null,
        confirmPromise: isReplyDisabled ? Promise.resolve(null) : confirmPostHistoryUrlOnly(fd, options)
      };
    }
    const type = options && options.isReply ? 'reply' : 'thread';
    const submittedAt = Date.now();
    const contentRaw = fd && fd.get ? String(fd.get('content') || '') : '';
    const contentText = normalizePostHistoryText(contentRaw);
    const resto = fd && fd.get ? String(fd.get('resto') || '').trim() : '';
    const fallbackFid = getCurrentPostHistoryFid();
    const localId = `local-${submittedAt}-${Math.random().toString(36).slice(2, 8)}`;
    const parsedSource = parseThreadHistoryUrl(location.href);
    const snapshot = {
      status: 'pending',
      type,
      localId,
      id: '',
      resto,
      threadId: type === 'reply' ? resto : '',
      postId: '',
      page: type === 'thread' ? (parsedSource ? parsedSource.page : 1) : 0,
      fid: fallbackFid,
      forumName: getPostHistoryForumNameByFid(fallbackFid),
      title: fd && fd.get ? String(fd.get('title') || '') : '',
      name: fd && fd.get ? String(fd.get('name') || '') : '',
      email: fd && fd.get ? String(fd.get('email') || '') : '',
      contentRaw,
      contentText,
      contentHash: hashPostHistoryText(contentText),
      userHash: '',
      submittedAt,
      confirmedAt: 0,
      sourceUrl: location.href,
      url: ''
    };
    // 为发串创建一个可等待的 Promise,用于确认后跳转
    let confirmResolver;
    const confirmPromise = new Promise(res => { confirmResolver = res; });
    postHistoryConfirmationMap.set(localId, confirmResolver);
    logPostHistory('snapshot', { snapshot: summarizePostHistorySnapshot(snapshot), content: summarizePostHistoryText(contentText) });
    upsertPostHistoryRecord(snapshot);
    completePostHistorySnapshot(localId, snapshot, 0);
    return { snapshot, localId, confirmPromise };
  }
  function parseThreadHistoryUrl(inputUrl) {
    let url;
    try {
      url = new URL(inputUrl || location.href, location.origin);
    } catch (e) {
      return null;
    }
    const path = url.pathname || '';
    // 普通串支持 /t/12345678 与 /t/12345678/page/323(不再识别笔误的 /t/12345678/323)
    const normalMatch = path.match(/\/t\/(\d{6,8})(?:\/page\/(\d+))?/);
    const poMatch = path.match(/\/Forum\/po\/id\/(\d{6,8})(?:\/page\/(\d+)\.html)?/);
    const match = normalMatch || poMatch;
    if (!match) return null;
    const pathPage = parseInt(match[2] || '', 10);
    const queryPage = parseInt(url.searchParams.get('page') || '', 10);
    return {
      mode: poMatch ? 'po' : 'normal',
      threadId: String(match[1]).slice(0, 8),
      page: pathPage > 0 ? pathPage : (queryPage > 0 ? queryPage : 1),
      url: url.toString()
    };
  }
  function buildThreadHistoryPageUrl(mode, threadId, page) {
    const tid = String(threadId || '').trim();
    const pageNum = Math.max(1, Number(page) || 1);
    if (!tid) return location.href;
    if (mode === 'po') return `${location.origin}/Forum/po/id/${tid}/page/${pageNum}.html`;
    // 普通串页码路径:/t/{id}/page/{n}(第 1 页保留 /t/{id})
    if (pageNum <= 1) return `${location.origin}/t/${tid}`;
    return `${location.origin}/t/${tid}/page/${pageNum}`;
  }
  function parseThreadHistoryPageNumberFromElement(el) {
    if (!el) return 0;
    const text = String(el.textContent || '').trim();
    const lastTextMatch = text.match(/末页\s*\((\d+)\)/);
    if (lastTextMatch) return Number(lastTextMatch[1]) || 0;
    const href = el.getAttribute && el.getAttribute('href');
    const parsed = href ? parseThreadHistoryUrl(href) : null;
    if (parsed && parsed.page) return Number(parsed.page) || 0;
    const numericText = text.match(/^\d+$/);
    return numericText ? Number(numericText[0]) || 0 : 0;
  }
  function getThreadHistoryPaginationBounds(root = document) {
    const paginations = Array.from((root || document).querySelectorAll('ul.uk-pagination.uk-pagination-left.h-pagination'));
    if (!paginations.length) return null;
    // 无缝翻页会给每页追加一个克隆分页栏;HTTP 缓存可能返回旧页面 → 某些栏的"末页"过时偏小。
    // 真实总页数 >= 任何栏显示的末页 → 跨所有栏取最大值,免疫缓存旧栏把 page/lastKnownPage 钢制回退
    let best = null;
    for (const pagination of paginations) {
      const items = Array.from(pagination.querySelectorAll('li'));
      const elements = Array.from(pagination.querySelectorAll('a, span'));
      const parsedLinks = elements
        .map(el => parseThreadHistoryUrl(el.getAttribute && el.getAttribute('href')))
        .filter(Boolean);
      const parsedIdentity = parsedLinks.find(parsed => parsed.threadId);
      const lastPageLink = elements.find(el => /^末页/.test(String(el.textContent || '').trim()));
      const activeEl = pagination.querySelector('li.uk-active a, li.uk-active span');
      const activePage = parseThreadHistoryPageNumberFromElement(activeEl);
      const nextItem = items.find(li => /下一页|下页|Next|›|»|→/i.test(String(li.textContent || '').trim()));
      const nextHasLink = !!(nextItem && nextItem.querySelector('a[href]'));
      const numericPages = elements
        .map(parseThreadHistoryPageNumberFromElement)
        .filter(num => num > 0);
      let lastPage = parseThreadHistoryPageNumberFromElement(lastPageLink);
      if (!lastPage && nextItem && !nextHasLink) {
        lastPage = activePage || Math.max(0, ...numericPages);
      }
      if (!lastPage) continue;
      const candidate = {
        lastPage,
        activePage,
        threadId: parsedIdentity && parsedIdentity.threadId || '',
        mode: parsedIdentity && parsedIdentity.mode || '',
        source: lastPageLink ? 'last-link' : 'disabled-next'
      };
      if (!best || lastPage > best.lastPage) best = candidate;
    }
    return best;
  }
  function applyThreadHistoryPageBounds(record, root = document, knownMaxPage = 0) {
    if (!record || !record.threadId) return record;
    const bounds = getThreadHistoryPaginationBounds(root);
    if (!bounds || !bounds.lastPage) return record;
    if (bounds.threadId && bounds.threadId !== String(record.threadId)) return record;
    if (bounds.mode && record.mode && bounds.mode !== record.mode) return record;
    // ── 过时快照写入闸门 ──
    // 串页数只增不减。分页栏快照末页 < 已确认进度(真实访问过的更后页) → 分页栏来自旧缓存
    // (整页 HTTP 缓存/无缝翻页克隆的旧栏), 据此钳制会把 page/maxVisitedPage/url 打回旧页码。
    // 拒绝本次快照的一切降级写入; 快照末页 ≥ 已知进度时才有资格参与钳制。
    const knownMax = Math.max(Number(knownMaxPage) || 0, 0);
    if (knownMax > 1 && bounds.lastPage < knownMax) return record;
    const parsedUrl = record.url ? parseThreadHistoryUrl(record.url) : null;
    const page = Math.max(1, Number(record.page || (parsedUrl && parsedUrl.page)) || 1);
    const boundedPage = Math.min(page, bounds.lastPage);
    const existingUrlPage = parsedUrl && parsedUrl.page || 0;
    const next = Object.assign({}, record, {
      page: boundedPage,
      maxVisitedPage: Math.min(Math.max(Number(record.maxVisitedPage) || boundedPage, boundedPage), bounds.lastPage),
      lastKnownPage: Math.max(Number(record.lastKnownPage) || 0, bounds.lastPage)
    });
    if (page > bounds.lastPage || existingUrlPage > bounds.lastPage) {
      next.url = buildThreadHistoryPageUrl(next.mode, next.threadId, boundedPage);
    }
    return next;
  }
  function getElementTextPreserveZeroWidth(el) {
    return el ? String(el.textContent || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n') : '';
  }
  function getVisibleTextForHistory(text) {
    return String(text || '').replace(ZERO_WIDTH_RE, '').replace(/[\s\u00a0]+/g, '');
  }
  function trimThreadHistoryContentText(text) {
    return String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').replace(/^[ \t\r\n]+/, '').replace(/[ \t\r\n]+$/, '');
    // 只去除ASCII空白(空格/Tab/换行),保留全角空格/零宽空格等用户输入的Unicode空白
  }
  function sanitizeThreadHistoryInlineStyle(styleValue) {
    const safeRules = [];
    String(styleValue || '').split(';').forEach(rule => {
      const separator = rule.indexOf(':');
      if (separator === -1) return;
      const name = rule.slice(0, separator).trim().toLowerCase();
      const value = rule.slice(separator + 1).trim();
      if (!/^(color|background-color|text-decoration|font-weight)$/.test(name) && !/^--darkreader-inline-(?:color|bgcolor)$/.test(name)) return;
      if (/url\s*\(|expression\s*\(|javascript:/i.test(value)) return;
      safeRules.push(`${name}: ${value}`);
    });
    return safeRules.join('; ');
  }
  function sanitizeThreadHistoryContentUrl(urlValue) {
    try {
      const url = new URL(urlValue, location.origin);
      if (!/^https?:$/.test(url.protocol)) return '';
      return url.href;
    } catch (e) {
      return '';
    }
  }
  function escapeThreadHistoryHtml(text) {
    return String(text || '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }
  function cleanThreadHistoryContentWhitespace(root) {
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    const nodes = [];
    while (walker.nextNode()) nodes.push(walker.currentNode);
    if (!nodes.length) return;
    // 首尾文本节点去空白 — 只清理ASCII空白(空格/Tab/换行),保留用户输入的全角空格/零宽空格等Unicode空白
    nodes[0].nodeValue = String(nodes[0].nodeValue || '').replace(/^[ \t\r\n]+/, '');
    const last = nodes[nodes.length - 1];
    last.nodeValue = String(last.nodeValue || '').replace(/[ \t\r\n]+$/, '');
  }
  function normalizeThreadHistoryContentWhitespace(root) {
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    const nodes = [];
    while (walker.nextNode()) nodes.push(walker.currentNode);
    nodes.forEach(node => {
      const prev = node.previousSibling;
      const next = node.nextSibling;
      let value = String(node.nodeValue || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
      // value = value.replace(/\n[ \t]*\n(?:[ \t]*\n)+/g, '\n\n'); // 连续空行压缩 — 已禁用,保留原始空行
      if (prev && prev.nodeType === Node.ELEMENT_NODE && prev.tagName === 'BR') value = value.replace(/^[ \t\r\n]+/, ''); // BR后空白清理 — 只清理ASCII空白,保留全角空格/零宽空格等用户输入的Unicode空白
      if (next && next.nodeType === Node.ELEMENT_NODE && next.tagName === 'BR') value = value.replace(/[ \t]+$/, ''); // BR前空白清理 — 保留:清理HTML源码格式化产生的空格
      node.nodeValue = value;
    });
    cleanThreadHistoryContentWhitespace(root);
  }
  function removeThreadHistoryTrailingBreaks(root) {
    while (root && root.lastChild) {
      const node = root.lastChild;
      if (node.nodeType === Node.ELEMENT_NODE) {
        removeThreadHistoryTrailingBreaks(node);
      }
      if (node.nodeType === Node.TEXT_NODE && !String(node.nodeValue || '').trim()) {
        node.remove();
        continue;
      }
      if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'BR') {
        node.remove();
        continue;
      }
      if (isEmptyThreadHistoryInlineElement(node)) {
        node.remove();
        continue;
      }
      break;
    }
  }
  function isEmptyThreadHistoryInlineElement(node) {
    if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
    if (!/^(A|SPAN|FONT|B|STRONG|I|EM|U|S|DEL|CODE|SUB|SUP)$/.test(node.tagName)) return false;
    return !String(node.textContent || '').trim() && !node.querySelector('img, video, audio, canvas, svg');
  }
  function limitThreadHistoryContentText(root, limit) {
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    const nodes = [];
    while (walker.nextNode()) nodes.push(walker.currentNode);
    let remaining = Math.max(0, Number(limit) || 0);
    let truncated = false;
    for (const node of nodes) {
      if (truncated || remaining <= 0) {
        node.nodeValue = '';
        continue;
      }
      const value = String(node.nodeValue || '');
      if (value.length > remaining) {
        node.nodeValue = value.slice(0, remaining).replace(/\s+$/, '');
        pruneAfterThreadHistoryTextNode(root, node);
        truncated = true;
        remaining = 0;
      } else {
        remaining -= value.length;
      }
    }
    cleanThreadHistoryContentWhitespace(root);
    removeThreadHistoryTrailingBreaks(root);
  }
  function pruneAfterThreadHistoryTextNode(root, textNode) {
    let current = textNode;
    while (current && current.parentNode && current !== root) {
      while (current.nextSibling) current.nextSibling.remove();
      current = current.parentNode;
    }
    if (current === root) removeThreadHistoryTrailingBreaks(root);
  }
  function isThreadHistoryContentTruncated(text) {
    return String(text || '').length > THREAD_HISTORY_EXCERPT_LIMIT;
  }
  function appendThreadHistoryTruncationMarker(contentEl) {
    if (!contentEl) return;
    removeThreadHistoryTrailingBreaks(contentEl);
    contentEl.appendChild(document.createTextNode('……'));
  }
  function sanitizeThreadHistoryInlineHtml(sourceEl) {
    if (!sourceEl) return '';
    const clone = sourceEl.cloneNode(true);
    clone.querySelectorAll('script, style, template, iframe, object, embed, svg, math').forEach(el => el.remove());
    const allowedTags = new Set(['SPAN', 'FONT', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'DEL', 'SUB', 'SUP']);
    Array.from(clone.querySelectorAll('*')).forEach(el => {
      if (!allowedTags.has(el.tagName)) {
        el.replaceWith(...Array.from(el.childNodes));
        return;
      }
      Array.from(el.attributes).forEach(attr => {
        const name = attr.name.toLowerCase();
        const value = attr.value || '';
        if (name === 'style') {
          const safeStyle = sanitizeThreadHistoryInlineStyle(value);
          if (safeStyle) el.setAttribute('style', safeStyle);
          else el.removeAttribute(attr.name);
          return;
        }
        if (el.tagName === 'FONT' && name === 'color') return;
        if (name === 'class') return;
        if (name === 'data-darkreader-inline-color' || name === 'data-darkreader-inline-bgcolor') return;
        el.removeAttribute(attr.name);
      });
    });
    cleanThreadHistoryContentWhitespace(clone);
    return clone.innerHTML.replace(/^[ \t\r\n]+/, '').replace(/[ \t\r\n]+$/, '');
    // 只去除ASCII空白,保留全角空格/零宽空格等用户输入的Unicode空白
  }
  function extractThreadHistoryCookieId(cookieEl, fallbackText) {
    if (cookieEl) {
      const font = cookieEl.querySelector('font');
      if (font) {
        for (const node of font.childNodes) {
          if (node.nodeType === Node.TEXT_NODE) {
            const value = String(node.nodeValue || '').trim();
            if (value) return value;
          }
        }
      }
    }
    const value = (String(fallbackText || '').split(':')[1] || fallbackText || '').trim();
    const match = String(value).match(/[A-Za-z0-9]{3,8}/);
    return match ? match[0] : value;
  }
  function buildThreadHistoryLegacyCookieHtml(cookieId) {
    const value = String(cookieId || '').trim();
    const match = value.match(/^([A-Za-z0-9]{3,8})(.+)$/);
    if (!match || !match[2].trim()) return '';
    const id = escapeThreadHistoryHtml(match[1]);
    const badge = escapeThreadHistoryHtml(match[2].trim());
    return `ID:<font color="red">${id}<sub style="color: darkorange; font-weight: bold;">${badge}</sub></font>`;
  }
  function getThreadHistoryCookieMarkId(item) {
    const value = String(item && item.cookieId || '').trim();
    const match = value.match(/^([A-Za-z0-9]{3,8})/);
    return match ? match[1] : value;
  }
  function sanitizeThreadHistoryContentHtml(contentEl) {
    if (!contentEl) return '';
    const clone = contentEl.cloneNode(true);
    clone.querySelectorAll('script, style, template, iframe, object, embed, svg, math').forEach(el => el.remove());
    const allowedTags = new Set(['A', 'BR', 'SPAN', 'FONT', 'B', 'STRONG', 'I', 'EM', 'U', 'S', 'DEL', 'CODE', 'PRE', 'SUB', 'SUP']);
    Array.from(clone.querySelectorAll('*')).forEach(el => {
      if (!allowedTags.has(el.tagName)) {
        el.replaceWith(...Array.from(el.childNodes));
        return;
      }
      Array.from(el.attributes).forEach(attr => {
        const name = attr.name.toLowerCase();
        const value = attr.value || '';
        if (name.startsWith('on') || name === 'id' || (name.startsWith('data-') && name !== 'data-darkreader-inline-color' && name !== 'data-darkreader-inline-bgcolor')) {
          el.removeAttribute(attr.name);
          return;
        }
        if (name === 'style') {
          const safeStyle = sanitizeThreadHistoryInlineStyle(value);
          if (safeStyle) el.setAttribute('style', safeStyle);
          else el.removeAttribute(attr.name);
          return;
        }
        if (el.tagName === 'A' && name === 'href') {
          const safeHref = sanitizeThreadHistoryContentUrl(value);
          if (safeHref) {
            el.setAttribute('href', safeHref);
            el.setAttribute('target', '_blank');
            el.setAttribute('rel', 'noopener');
          } else {
            el.removeAttribute(attr.name);
          }
          return;
        }
        if (el.tagName === 'FONT' && name === 'color') return;
        if (name !== 'class' && name !== 'title' && name !== 'target' && name !== 'rel') el.removeAttribute(attr.name);
      });
    });
    normalizeThreadHistoryContentWhitespace(clone);
    return clone.innerHTML.replace(/^[ \t\r\n]+/, '').replace(/[ \t\r\n]+$/, '');
    // 只去除ASCII空白,保留全角空格/零宽空格等用户输入的Unicode空白
  }
  function normalizeThreadHistoryImageFile(urlValue) {
    if (!urlValue) return '';
    try {
      const url = new URL(urlValue, location.origin);
      const match = url.pathname.match(/\/(?:image|thumb)\/([^?#]+)/);
      return match ? decodeURIComponent(match[1]) : '';
    } catch (e) {
      const match = String(urlValue).match(/\/(?:image|thumb)\/([^?#]+)/);
      return match ? decodeURIComponent(match[1]) : '';
    }
  }
  const THREAD_HISTORY_IMAGE_FILE_CONTRACT_EXAMPLE = '2024-12-10/6757ea866e1aa.png';
  function extractThreadHistoryImageFile(mainEl) {
    if (!mainEl) return '';
    const anchor = mainEl.querySelector('.h-threads-img-a[href]');
    const img = mainEl.querySelector('.h-threads-img-a img, img.h-threads-img');
    const sources = [
      anchor && anchor.getAttribute('href'),
      img && img.dataset && img.dataset.xdexHdSrc,
      img && img.dataset && img.dataset.xdexThumbSrc,
      img && img.dataset && img.dataset.src,
      img && img.getAttribute && img.getAttribute('src')
    ];
    for (const source of sources) {
      const imageFile = normalizeThreadHistoryImageFile(source);
      if (imageFile) return imageFile;
    }
    return '';
  }
  function isThreadHistoryMainCandidate(el) {
    return !!el && !el.closest('.h-preview-box') && !!el.querySelector('.h-threads-content');
  }
  function findThreadHistoryMainElement(root, parsed) {
    const scope = root || document;
    const primary = scope.querySelector('.h-threads-list .h-threads-item-main');
    if (isThreadHistoryMainCandidate(primary)) return primary;
    const mains = Array.from(scope.querySelectorAll('.h-threads-item-main'));
    return mains.find(isThreadHistoryMainCandidate) || null;
  }
  function extractThreadHistoryRecord(root) {
    const parsed = parseThreadHistoryUrl(location.href);
    if (!parsed) {
      logThreadHistory('skip record: unsupported url', { url: location.href });
      return null;
    }
    const mainEl = findThreadHistoryMainElement(root, parsed);
    if (!mainEl) {
      logThreadHistory('skip record: missing h-threads-item-main', { url: location.href, parsed });
      return null;
    }
    const contentEl = mainEl.querySelector('.h-threads-content');
    const rawContent = getElementTextPreserveZeroWidth(contentEl);
    const contentText = trimThreadHistoryContentText(rawContent);
    const contentTruncated = isThreadHistoryContentTruncated(contentText);
    const contentHtml = sanitizeThreadHistoryContentHtml(contentEl);
    const hasZeroWidth = ZERO_WIDTH_RE.test(rawContent);
    const hasVisibleText = !!getVisibleTextForHistory(rawContent);
    const hasWhitespaceOnly = !hasVisibleText && /^[\s\u00a0]*$/.test(rawContent.replace(ZERO_WIDTH_RE, '')) && rawContent.length > 0;
    const imageFile = extractThreadHistoryImageFile(mainEl);
    const title = getElementTextPreserveZeroWidth(mainEl.querySelector('.h-threads-info-title')).trim();
    const author = getElementTextPreserveZeroWidth(mainEl.querySelector('.h-threads-info-email')).trim();
    const cookieEl = mainEl.querySelector('.h-threads-info-uid');
    const cookieText = getElementTextPreserveZeroWidth(cookieEl).trim();
    const cookieId = extractThreadHistoryCookieId(cookieEl, cookieText);
    const cookieHtml = sanitizeThreadHistoryInlineHtml(cookieEl);
    const createdAtEl = mainEl.querySelector('.h-threads-info-createdat, .h-threads-info time');
    const createdAt = String(createdAtEl && (createdAtEl.getAttribute('title') || createdAtEl.getAttribute('datetime')) || getElementTextPreserveZeroWidth(createdAtEl)).trim();
    const tipsEl = mainEl.querySelector('.h-threads-tips');
    const sageHtml = (tipsEl && /SAGE/i.test(tipsEl.textContent || ''))
      ? '<i class="uk-icon-thumbs-down"></i>&nbsp;本串已经被SAGE'
      : '';
    return {
      key: getThreadHistoryKey(parsed.mode, parsed.threadId),
      mode: parsed.mode,
      threadId: parsed.threadId,
      page: parsed.page,
      url: parsed.url,
      title,
      author,
      cookieId,
      cookieHtml,
      createdAt,
      contentText,
      contentHtml,
      contentTruncated,
      excerpt: contentText.slice(0, THREAD_HISTORY_EXCERPT_LIMIT),
      imageFile,
      contentFlags: { hasVisibleText, hasWhitespaceOnly, hasZeroWidth },
      sageHtml,
            lastScrollY: Math.max(0, Math.floor(window.scrollY || 0))
    };
  }
  // 索引指纹:覆盖 searchText 与全部布尔谓词的来源字段;仅短标量拼接,远廉于重建 searchText
  function threadHistoryIndexFingerprint(item) {
    const flags = (item && item.contentFlags) || {};
    return [
      item && item.threadId, item && item.mode, item && item.title, item && item.author,
      item && item.cookieId, String(item && (item.contentText || item.excerpt) || '').length,
      item && item.imageFile, flags.hasZeroWidth ? 1 : 0, flags.hasVisibleText ? 1 : 0,
      flags.hasWhitespaceOnly ? 1 : 0, item && item.sageHtml ? 1 : 0,
      Number(item && item.lastVisitedAt) || 0
    ].join('\u0001');
  }
  function buildThreadHistoryIndexEntry(item) {
    const contentFlags = item && item.contentFlags ? item.contentFlags : {};
    const imageFile = String(item && item.imageFile || '');
    const titleText = String(item && item.title || '').toLowerCase();
    const authorText = String(item && item.author || '').toLowerCase();
    const cookieIdText = String(item && item.cookieId || '').toLowerCase();
    const excerptText = String(item && (item.contentText || item.excerpt) || '').toLowerCase();
    const threadIdText = String(item && item.threadId || '');
    return {
      fp: threadHistoryIndexFingerprint(item),
      searchText: [threadIdText, titleText, authorText, cookieIdText, excerptText].join(' ').toLowerCase(),
      threadIdText,
      titleText,
      authorText,
      cookieIdText,
      excerptText,
      mode: item && item.mode === 'po' ? 'po' : 'normal',
      hasImage: !!imageFile,
      isGif: /\.gif(?:$|[?#])/i.test(imageFile),
      hasZeroWidth: !!contentFlags.hasZeroWidth,
      hasVisibleText: !!contentFlags.hasVisibleText,
      hasWhitespaceOnly: !!contentFlags.hasWhitespaceOnly,
      isSage: !!(item && item.sageHtml),
      lastVisitedAt: Number(item && item.lastVisitedAt) || 0
    };
  }
  function upsertThreadHistoryRecord(nextRecord, options = {}) {
    if (!nextRecord || !nextRecord.threadId || !nextRecord.mode) return getThreadHistoryStore();
    const now = Date.now();
    const countVisit = options.countVisit !== false;
    const touchVisitedAt = countVisit || options.touchVisitedAt === true;
    const store = getThreadHistoryStore();
    // 写前校验: 以存量进度为信任下限。过时分页快照(lastPage < 已到过的最远页)在 bounds 闸门被整条拒绝,
    // 不会再把 page/maxVisitedPage/url 打回旧页码
    const key = nextRecord.key || getThreadHistoryKey(nextRecord.mode, nextRecord.threadId);
    const old = store.items[key] || {};
    const knownMax = Math.max(Number(old.maxVisitedPage) || 0, Number(old.page) || 0);
    nextRecord = applyThreadHistoryPageBounds(nextRecord, document, knownMax);
    // maxVisitedPage 单调只升不降(lastKnownPage 钳制在闸门之后仅剩抬升作用)
    const maxVisitedPage = Math.max(Number(old.maxVisitedPage) || 1, Number(nextRecord.page) || 1);
    const mergedBase = Object.assign({}, old, nextRecord);
    const merged = Object.assign({}, applyThreadHistoryPageBounds(mergedBase, document, knownMax), {
      key,
      firstVisitedAt: old.firstVisitedAt || now,
      lastVisitedAt: touchVisitedAt ? now : (Number(old.lastVisitedAt) || now),
      visitCount: (Number(old.visitCount) || 0) + (countVisit ? 1 : 0),
      maxVisitedPage,
      cookieHtml: nextRecord.cookieHtml || old.cookieHtml || ''
    });
    store.items[key] = merged;
    store.order = [key].concat((store.order || []).filter(itemKey => itemKey !== key));
    const saved = setThreadHistoryStore(store);
    logThreadHistory('record saved', { key, total: saved.order.length, countVisit, reason: options.reason || '', record: JSON.parse(JSON.stringify({ page: merged.page, maxVisitedPage: merged.maxVisitedPage, lastKnownPage: merged.lastKnownPage, url: merged.url, visitCount: merged.visitCount })) });
    // 同步常用串菜单中对应串的链接
    try { if (typeof syncFavoriteThreadsLinks === 'function') syncFavoriteThreadsLinks(); } catch (e) {}
    return saved;
  }
  function touchThreadHistoryCurrentLocation(options = {}) {
    if (!isThreadHistoryEnabledNow()) return getThreadHistoryStore(); // 浏览历史已关闭:不更新位置
    const parsed = parseThreadHistoryUrl(options.url || location.href);
    if (!parsed) return getThreadHistoryStore();
    const key = getThreadHistoryKey(parsed.mode, parsed.threadId);
    const store = getThreadHistoryStore();
    const item = store.items[key];
    if (!item) return store;
    const now = Date.now();
    // 写前校验: 以存量进度为信任下限, 过时分页快照不得降级写入; lastKnownPage/maxVisitedPage 单调只升
    const knownMax = Math.max(Number(item.maxVisitedPage) || 0, Number(item.page) || 0);
    // page = 最近查看页, 如实记录当前所在页(允许主动回退到更低页码); maxVisitedPage 不受回翻影响
    const bounded = applyThreadHistoryPageBounds(Object.assign({}, item, {
      page: Number(options.page || parsed.page) || 1,
      url: options.url || parsed.url
    }), document, knownMax);
    item.page = bounded.page;
    item.url = bounded.url || item.url;
    item.maxVisitedPage = Math.max(Number(item.maxVisitedPage) || 1, Number(item.page) || 1);
    if (bounded.lastKnownPage && Number(bounded.lastKnownPage) > (Number(item.lastKnownPage) || 0)) item.lastKnownPage = bounded.lastKnownPage;
    item.lastScrollY = Math.max(0, Math.floor(window.scrollY || 0));
    if (options.touchVisitedAt) item.lastVisitedAt = now;
    store.index[key] = buildThreadHistoryIndexEntry(item);
    store.order = [key].concat((store.order || []).filter(itemKey => itemKey !== key));
    const saved = setThreadHistoryStore(store);
    logThreadHistory('location touched', { key, reason: options.reason || '', page: item.page, url: item.url, maxVisitedPage: item.maxVisitedPage, touchVisitedAt: !!options.touchVisitedAt });
    // 同步常用串菜单中对应串的链接
    try { if (typeof syncFavoriteThreadsLinks === 'function') syncFavoriteThreadsLinks(); } catch (e) {}
    return saved;
  }
  function recordThreadHistoryProgress(options = {}) {
    if (!isThreadHistoryEnabledNow()) return getThreadHistoryStore(); // 浏览历史已关闭:不记录翻页进度
    const parsed = parseThreadHistoryUrl(options.url || location.href);
    if (!parsed) return getThreadHistoryStore();
    const record = extractThreadHistoryRecord(document) || {
      key: getThreadHistoryKey(parsed.mode, parsed.threadId),
      mode: parsed.mode,
      threadId: parsed.threadId
    };
    record.key = getThreadHistoryKey(parsed.mode, parsed.threadId);
    record.mode = parsed.mode;
    record.threadId = parsed.threadId;
    record.page = Math.max(Number(record.page) || 1, Number(options.page || parsed.page) || 1);
    record.url = options.url || parsed.url;
    record.lastScrollY = Math.max(0, Math.floor(window.scrollY || 0));
    return upsertThreadHistoryRecord(record, { countVisit: false, touchVisitedAt: options.touchVisitedAt === true, reason: options.reason || 'progress' });
  }
  function updateThreadHistoryScrollPosition() {
    touchThreadHistoryCurrentLocation({ reason: 'scroll-position' });
  }
  function deleteThreadHistoryItem(key) {
    const store = getThreadHistoryStore();
    // 软删除(分家): 原记录整体搬入墓碑快照, 主列表移除; 30 天内可恢复, 到期自动降级
    const item = store.items[key];
    if (item) {
      store.tombstones = store.tombstones || {};
      const prev = store.tombstones[key];
      store.tombstones[key] = {
        deletedAt: Date.now(),
        origin: 'local',
        purged: false,
        revivedAt: null,
        record: Object.assign({}, item)
      };
      void prev;
      try {
        const bl = GM_getValue('xdex_webdav_history_baselines', null);
        if (bl && bl.threadHistory && (key in bl.threadHistory)) { delete bl.threadHistory[key]; GM_setValue('xdex_webdav_history_baselines', bl); }
      } catch (e) {}
    }
    delete store.items[key];
    delete store.index[key];
    store.order = (store.order || []).filter(itemKey => itemKey !== key);
    return setThreadHistoryStore(store);
  }
  function restoreThreadHistoryFromTombstone(key) {
    const store = getThreadHistoryStore();
    const tomb = (store.tombstones || {})[key];
    if (!tomb || tomb.purged || tomb.revivedAt) return false;
    const record = tomb.record || null;
    const shadow = store.items[key] || null;   // 删除后新段(如有)
    let merged = null;
    if (record && shadow) {
      const recNewer = (Number(record.lastVisitedAt) || 0) >= (Number(shadow.lastVisitedAt) || 0);
      merged = Object.assign({}, shadow, record, {
        firstVisitedAt: Math.min(Number(record.firstVisitedAt) || Infinity, Number(shadow.firstVisitedAt) || Infinity),
        lastVisitedAt: Math.max(Number(record.lastVisitedAt) || 0, Number(shadow.lastVisitedAt) || 0),
        page: Math.max(Number(record.page) || 0, Number(shadow.page) || 0),
        maxVisitedPage: Math.max(Number(record.maxVisitedPage) || 0, Number(shadow.maxVisitedPage) || 0),
        visitCount: (Number(record.visitCount) || 0) + (Number(shadow.visitCount) || 0),
        title: recNewer ? (record.title || shadow.title) : (shadow.title || record.title),
        name: recNewer ? (record.name || shadow.name) : (shadow.name || record.name)
      });
    } else {
      merged = record ? Object.assign({}, record) : (shadow ? Object.assign({}, shadow) : null);
    }
    if (merged) {
      store.items[key] = merged;
      store.index[key] = buildThreadHistoryIndexEntry(merged);
      if (!(store.order || []).includes(key)) store.order.push(key);
    }
    // 墓碑转为 revivedAt 传播标记: 对端据此执行同样的恢复合并
    store.tombstones = store.tombstones || {};
    store.tombstones[key] = { deletedAt: Number(tomb.deletedAt) || Date.now(), origin: tomb.origin || 'local', purged: false, revivedAt: Date.now() };
    // 基线重置为恢复后的全局值, 防止下轮 delta 虚算
    if (merged) {
      try {
        const bl = GM_getValue('xdex_webdav_history_baselines', null) || {};
        bl.threadHistory = bl.threadHistory || {};
        bl.threadHistory[key] = { count: Number(merged.visitCount) || 0, at: Date.now() };
        GM_setValue('xdex_webdav_history_baselines', bl);
      } catch (e) {}
    } else {
      deleteThreadHistoryBaselineEntry(key);
    }
    return setThreadHistoryStore(store);
  }
  function purgeThreadHistoryTombstone(key) {
    const store = getThreadHistoryStore();
    const tomb = (store.tombstones || {})[key];
    if (!tomb) return false;
    // 立即删除: 快照与新段一并清除, 仅留压制标记(purged), 防止对端旧数据复活
    delete store.items[key];
    delete store.index[key];
    store.order = (store.order || []).filter(itemKey => itemKey !== key);
    store.tombstones[key] = { deletedAt: Number(tomb.deletedAt) || Date.now(), origin: tomb.origin || 'local', purged: true };
    deleteThreadHistoryBaselineEntry(key);
    return setThreadHistoryStore(store);
  }
  function purgeAllThreadHistoryTombstones() {
    const store = getThreadHistoryStore();
    const tombs = store.tombstones || {};
    const now = Date.now();
    Object.keys(tombs).forEach((key) => {
      if (tombs[key].purged) return;
      delete store.items[key];
      delete store.index[key];
      tombs[key] = { deletedAt: Number(tombs[key].deletedAt) || now, origin: tombs[key].origin || 'local', purged: true };
      deleteThreadHistoryBaselineEntry(key);
    });
    store.order = (store.order || []).filter(k => !tombs[k] || tombs[k].purged);
    return setThreadHistoryStore(store);
  }
  function deleteThreadHistoryBaselineEntry(key) {
    try {
      const bl = GM_getValue('xdex_webdav_history_baselines', null);
      if (!bl || !bl.threadHistory || !(key in bl.threadHistory)) return;
      delete bl.threadHistory[key];
      GM_setValue('xdex_webdav_history_baselines', bl);
    } catch (e) {}
  }
  function clearThreadHistory() {
    // 批量软删除: 全部条目移入回收站(逐条墓碑), 30 天后自动彻底清除; 期间可恢复
    const store = getThreadHistoryStore();
    const now = Date.now();
    store.tombstones = store.tombstones || {};
    Object.keys(store.items).forEach((key) => {
      if (store.tombstones[key] && !store.tombstones[key].purged && !store.tombstones[key].revivedAt) return;
      store.tombstones[key] = { deletedAt: now, origin: 'local', purged: false, revivedAt: null, record: Object.assign({}, store.items[key]) };
      deleteThreadHistoryBaselineEntry(key);
    });
    store.items = {};
    store.index = {};
    store.order = [];
    return setThreadHistoryStore(store);
  }
  function parseThreadHistorySearchQuery(query) {
    const filters = { mode: '', hasImage: false, isGif: false, hasZeroWidth: false, isSage: false };
    const tokens = [];
    String(query || '').toLowerCase().split(/\s+/).filter(Boolean).forEach(token => {
      if (token === 'mode:po') filters.mode = 'po';
      else if (token === 'mode:normal') filters.mode = 'normal';
      else if (token === 'has:image') filters.hasImage = true;
      else if (token === 'has:gif') filters.isGif = true;
      else if (token === 'has:zwsp' || token === 'has:zerowidth') filters.hasZeroWidth = true;
      else if (token === 'has:sage') filters.isSage = true;
      else tokens.push(token);
    });
    return { filters, tokens };
  }
  function scoreThreadHistoryIndexEntry(entry, numericTokens) {
    let score = Number(entry.lastVisitedAt) || 0;
    for (let i = 0; i < numericTokens.length; i++) {
      const token = numericTokens[i];
      if (entry.threadIdText === token) score += 1000000000000000;
      else if (entry.threadIdText.includes(token)) score += 500000000000000;
    }
    return score;
  }
  function getThreadHistorySortValue(item, field) {
    if (!item) return 0;
    if (field === 'visitCount') return Number(item.visitCount) || 0;
    if (field === 'maxVisitedPage') return Number(item.maxVisitedPage || item.page) || 0;
    return Number(item.lastVisitedAt) || 0;
  }
  function compareThreadHistoryResults(a, b, sortMode, numericTokens) {
    const itemA = a.item || {};
    const itemB = b.item || {};
    if (sortMode === 'last-asc') return getThreadHistorySortValue(itemA, 'lastVisitedAt') - getThreadHistorySortValue(itemB, 'lastVisitedAt');
    if (sortMode === 'visits-desc') return getThreadHistorySortValue(itemB, 'visitCount') - getThreadHistorySortValue(itemA, 'visitCount') || getThreadHistorySortValue(itemB, 'lastVisitedAt') - getThreadHistorySortValue(itemA, 'lastVisitedAt');
    if (sortMode === 'visits-asc') return getThreadHistorySortValue(itemA, 'visitCount') - getThreadHistorySortValue(itemB, 'visitCount') || getThreadHistorySortValue(itemB, 'lastVisitedAt') - getThreadHistorySortValue(itemA, 'lastVisitedAt');
    if (sortMode === 'page-desc') return getThreadHistorySortValue(itemB, 'maxVisitedPage') - getThreadHistorySortValue(itemA, 'maxVisitedPage') || getThreadHistorySortValue(itemB, 'lastVisitedAt') - getThreadHistorySortValue(itemA, 'lastVisitedAt');
    // 分数已在检索期预计算;兜底按需再算
    const scoreA = a.score != null ? a.score : scoreThreadHistoryIndexEntry(a.index, numericTokens || []);
    const scoreB = b.score != null ? b.score : scoreThreadHistoryIndexEntry(b.index, numericTokens || []);
    return scoreB - scoreA;
  }
  function searchThreadHistory(query, storeInput, sortMode) {
    // 默认走记忆化缓存:仅显式传入 storeInput 时才归一化,避免每次检索重复归一化 + 重复排序
    const store = storeInput ? normalizeThreadHistoryStore(storeInput) : getThreadHistoryStore();
    const { filters, tokens } = parseThreadHistorySearchQuery(query);
    // 数字 token 判定外提一次:原先在比较器内每次比较都跑正则(约 2·m·log m·tokens 次)
    const numericTokens = tokens.filter(token => /^\d{1,8}$/.test(token));
    const useScoreSort = !sortMode || sortMode === 'last-desc';
    return (store.order || [])
      .filter(key => {
        const entry = store.index[key];
        if (!entry || !store.items[key]) return false;
        // 回收站压制: 存在未复活墓碑的条目不在主列表显示(删除后新段同样隐藏, 恢复后随 revivedAt 解除)
        const tomb = store.tombstones && store.tombstones[key];
        // 仅"无删除后新段"的纯墓碑才隐藏; 再次浏览会重建 items 影子条目, 带回收站标识重新显示
        if (tomb && !tomb.revivedAt && !store.items[key]) return false;
        if (filters.mode && entry.mode !== filters.mode) return false;
        if (filters.hasImage && !entry.hasImage) return false;
        if (filters.isGif && !entry.isGif) return false;
        if (filters.hasZeroWidth && !entry.hasZeroWidth) return false;
        if (filters.isSage && !entry.isSage) return false;
        return tokens.every(token => entry.searchText.includes(token));
      })
      .map(key => {
        const index = store.index[key];
        return { key, item: store.items[key], index, score: useScoreSort ? scoreThreadHistoryIndexEntry(index, numericTokens) : 0 };
      })
      .sort((a, b) => compareThreadHistoryResults(a, b, sortMode || 'last-desc', numericTokens));
  }
  let threadHistoryScrollTrackingInstalled = false;
  function installThreadHistoryScrollTracking() {
    if (threadHistoryScrollTrackingInstalled) return;
    threadHistoryScrollTrackingInstalled = true;
    let scrollTimer = 0;
    window.addEventListener('scroll', () => {
      if (scrollTimer) return;
      scrollTimer = setTimeout(() => {
        scrollTimer = 0;
        updateThreadHistoryScrollPosition();
      }, 1200);
    }, { passive: true });
    window.addEventListener('pagehide', updateThreadHistoryScrollPosition, { passive: true });
  }
  function isThreadHistoryPageActive() {
    return document.visibilityState === 'visible' && (typeof document.hasFocus !== 'function' || document.hasFocus());
  }
  function cancelThreadHistoryDwellTimer(resetSession) {
    if (threadHistoryDwellTimer) clearTimeout(threadHistoryDwellTimer);
    threadHistoryDwellTimer = 0;
    threadHistoryVisibleSince = 0;
    if (resetSession) threadHistoryVisibleSessionCounted = false;
  }
  function scheduleThreadHistoryReactivationVisit(source) {
    if (!parseThreadHistoryUrl(location.href)) return;
    if (!isThreadHistoryPageActive()) {
      cancelThreadHistoryDwellTimer(document.visibilityState !== 'visible');
      return;
    }
    if (threadHistoryVisibleSessionCounted || threadHistoryDwellTimer) return;
    threadHistoryVisibleSince = Date.now();
    updateThreadHistoryDebugState({ lastDwell: { source, status: 'scheduled', threshold: THREAD_HISTORY_REVISIT_DWELL_MS, at: new Date().toISOString() } });
    threadHistoryDwellTimer = setTimeout(() => {
      threadHistoryDwellTimer = 0;
      if (!threadHistoryVisibleSince || !isThreadHistoryPageActive()) return;
      if (Date.now() - threadHistoryVisibleSince < THREAD_HISTORY_REVISIT_DWELL_MS) return;
      threadHistoryVisibleSessionCounted = true;
      updateThreadHistoryDebugState({ lastDwell: { source, status: 'counted', threshold: THREAD_HISTORY_REVISIT_DWELL_MS, at: new Date().toISOString() } });
      recordCurrentThreadHistory(0, { reason: 'reactivation-dwell', countVisit: true });
    }, THREAD_HISTORY_REVISIT_DWELL_MS);
  }
  function installThreadHistoryReactivationTracking(initialCounted) {
    if (threadHistoryReactivationTrackingInstalled) return;
    threadHistoryReactivationTrackingInstalled = true;
    threadHistoryVisibleSessionCounted = !!initialCounted && document.visibilityState === 'visible';
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        scheduleThreadHistoryReactivationVisit('visibilitychange');
        // ★ 补跑全量加宽(QwQNT 时序自愈):初始 pass 可能命中 h-active 静默跳过 /
        //   offsetWidth=0 RO 自愈 / targetWidth<=0 静默返回,这些分支失败时无锁无兜底;
        //   QwQNT 宿主在标签切换时会派发正确的 visibilitychange,此处即自愈触发器。
        //   __imageWidthExpanded 锁保证已加宽者 O(1) 跳过;函数未初始化时可选链静默跳过
        try { enableHDImageAndLayoutFix.expandReplyMainWidths?.(document); } catch (e) {}
      } else {
        cancelThreadHistoryDwellTimer(true);
      }
    }, { passive: true });
    window.addEventListener('focus', () => scheduleThreadHistoryReactivationVisit('focus'), { passive: true });
    window.addEventListener('blur', () => cancelThreadHistoryDwellTimer(false), { passive: true });
    window.addEventListener('pagehide', () => cancelThreadHistoryDwellTimer(true), { passive: true });
  }
  // ★ 历史功能开关读取(即时生效:优先 SettingPanel.state,未初始化时回退 defaults)
  function isThreadHistoryEnabledNow() {
    try {
      const sp = typeof SettingPanel !== 'undefined' ? SettingPanel : null;
      if (!sp) return true;
      const state = sp.state || {};
      if (typeof state.enableThreadHistory === 'boolean') return state.enableThreadHistory;
      const defaults = sp.defaults || {};
      return defaults.enableThreadHistory !== false;
    } catch (e) { return true; }
  }
  function isPostHistoryEnabledNow() {
    try {
      const sp = typeof SettingPanel !== 'undefined' ? SettingPanel : null;
      if (!sp) return true;
      const state = sp.state || {};
      if (typeof state.enablePostHistory === 'boolean') return state.enablePostHistory;
      const defaults = sp.defaults || {};
      return defaults.enablePostHistory !== false;
    } catch (e) { return true; }
  }
  function recordCurrentThreadHistory(attempt = 0, options = {}) {
    if (!isThreadHistoryEnabledNow()) return; // 浏览历史已关闭:不再自动记录(已有数据保留)
    const record = extractThreadHistoryRecord(document);
    if (!record) {
      const parsed = parseThreadHistoryUrl(location.href);
      const missingMain = !!parsed && !findThreadHistoryMainElement(document, parsed);
      updateThreadHistoryDebugState({
        lastRecord: {
          status: parsed ? (missingMain ? 'missing-main' : 'extract-failed') : 'unsupported-url',
          attempt,
          parsed,
          at: new Date().toISOString(),
          readyState: document.readyState,
          mainCount: document.querySelectorAll('.h-threads-item-main').length,
          listCount: document.querySelectorAll('.h-threads-list').length
        }
      });
      if (missingMain && attempt < THREAD_HISTORY_RECORD_RETRY_LIMIT) {
        logThreadHistory('retry record: waiting for h-threads-item-main', {
          attempt: attempt + 1,
          limit: THREAD_HISTORY_RECORD_RETRY_LIMIT,
          delay: THREAD_HISTORY_RECORD_RETRY_DELAY,
          readyState: document.readyState
        });
        setTimeout(() => recordCurrentThreadHistory(attempt + 1), THREAD_HISTORY_RECORD_RETRY_DELAY);
      }
      return;
    }
    upsertThreadHistoryRecord(record, Object.assign({ reason: 'initial-load', countVisit: true }, options));
    updateThreadHistoryDebugState({ lastRecord: { status: 'saved', attempt, reason: options.reason || 'initial-load', record, at: new Date().toISOString() } });
    installThreadHistoryScrollTracking();
  }
  function formatThreadHistoryTime(ts) {
    if (!ts) return '';
    const d = new Date(ts);
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    const hh = String(d.getHours()).padStart(2, '0');
    const mm = String(d.getMinutes()).padStart(2, '0');
    return `${y}-${m}-${day} ${hh}:${mm}`;
  }
  function formatRelativeTimeMachineTime(ts) {
    if (!ts) return '';
    const d = new Date(ts);
    if (Number.isNaN(d.getTime())) return '';
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    const weekday = '日一二三四五六'[d.getDay()];
    const hh = String(d.getHours()).padStart(2, '0');
    const mm = String(d.getMinutes()).padStart(2, '0');
    const ss = String(d.getSeconds()).padStart(2, '0');
    return `${y}-${m}-${day}(${weekday})${hh}:${mm}:${ss}`;
  }
  function buildThreadHistoryImageUrl(imageFile, full) {
    if (!imageFile) return '';
    const path = /\.gif$/i.test(imageFile) || full ? 'image' : 'thumb';
    const encodedFile = String(imageFile).split('/').map(encodeURIComponent).join('/');
    return `https://image.nmb.best/${path}/${encodedFile}`;
  }
  function buildThreadHistoryItemUrl(item) {
    // 消费一致性: 不信任持久化的 item.url(可能被旧快照/裸展开污染, 出现 page=97/url=?page=86 的矛盾)。
    // 链接指向最近查看页(item.page, 允许低于 maxVisitedPage 的主动回翻); 仅当无法重建时才回退 item.url
    if (item && item.page && item.threadId) {
      return buildThreadHistoryPageUrl(item.mode, item.threadId, item.page);
    }
    if (item && item.url) return item.url;
    const threadId = item && item.threadId ? item.threadId : '';
    const page = item && item.page ? item.page : 1;
    return buildThreadHistoryPageUrl(item && item.mode, threadId, page);
  }
  function buildHistorySearchHelpMark(title) {
    const mark = document.createElement('span');
    mark.className = 'xdex-history-search-help';
    mark.textContent = '?';
    mark.title = title || '';
    mark.style.textDecoration = 'underline';
    mark.style.cursor = 'help';
    mark.style.whiteSpace = 'nowrap';
    mark.setAttribute('aria-label', title || '高级检索说明');
    return mark;
  }
  function getLatestThreadHistoryUrl(threadId) {
    const tid = String(threadId || '').trim();
    if (!isValidThreadId(tid)) return '';
    const store = getThreadHistoryStore();
    // 常用串/浏览历史联动:优先普通模式,没有再回退只看 PO;都没有返回空(调用方回退普通第 1 页)
    const normal = store.items[getThreadHistoryKey('normal', tid)];
    if (normal) return buildThreadHistoryItemUrl(normal);
    const po = store.items[getThreadHistoryKey('po', tid)];
    return po ? buildThreadHistoryItemUrl(po) : '';
  }
  function appendThreadHistoryText(parent, tagName, className, text) {
    const el = document.createElement(tagName);
    if (className) el.className = className;
    el.textContent = text || '';
    parent.appendChild(el);
    return el;
  }
  function appendThreadHistoryInfoText(parent, className, text) {
    const value = String(text || '').trim();
    if (!value) return null;
    return appendThreadHistoryText(parent, 'span', className, value);
  }
  function shouldRenderThreadHistoryTitle(title) {
    const value = String(title || '').trim();
    return !!value && value !== '无标题';
  }
  function shouldRenderThreadHistoryAuthor(author) {
    const value = String(author || '').trim();
    return !!value && value !== '无名氏';
  }
  function buildThreadHistoryItemElement(result) {
    const item = result.item || {};
    const wrapper = document.createElement('div');
    wrapper.className = 'xdex-history-item';
    wrapper.dataset.historyKey = result.key;
    const main = document.createElement('div');
    main.className = 'h-threads-item-main';
    wrapper.appendChild(main);
    const info = document.createElement('div');
    info.className = 'h-threads-info xdex-history-info';
    main.appendChild(info);
    const infoMain = document.createElement('span');
    infoMain.className = 'xdex-history-info-main';
    info.appendChild(infoMain);
    if (shouldRenderThreadHistoryTitle(item.title)) appendThreadHistoryInfoText(infoMain, 'h-threads-info-title', item.title);
    if (shouldRenderThreadHistoryAuthor(item.author)) appendThreadHistoryInfoText(infoMain, 'h-threads-info-email', item.author);
    const createdAtNode = appendThreadHistoryInfoText(infoMain, 'h-threads-info-createdat', item.createdAt);
    if (createdAtNode) {
      createdAtNode.dataset.xdexOriginalTime = item.createdAt;
      createdAtNode.title = item.createdAt;
    }
    const cookieHtml = item.cookieHtml || buildThreadHistoryLegacyCookieHtml(item.cookieId);
    const cookieMarkId = getThreadHistoryCookieMarkId(item);
    if (cookieHtml) {
      const cookieSpan = appendThreadHistoryText(infoMain, 'span', 'h-threads-info-uid', '');
      if (cookieMarkId) cookieSpan.setAttribute('data-xdex-cookie-id', cookieMarkId);
      cookieSpan.innerHTML = cookieHtml;
    } else if (item.cookieId) {
      const cookieSpan = appendThreadHistoryInfoText(infoMain, 'h-threads-info-uid', `ID:${item.cookieId}`);
      if (cookieSpan && cookieMarkId) cookieSpan.setAttribute('data-xdex-cookie-id', cookieMarkId);
    }
    const historyReplyUrl = buildCanonicalReplyUrl(item.threadId, item.threadId);
    const historyReplyActionUrl = buildThreadHistoryItemUrl(item);
    const replyLink = document.createElement('a');
    replyLink.className = 'h-threads-info-id xdex-history-thread-id';
    replyLink.href = historyReplyUrl;
    replyLink.textContent = `No.${item.threadId || ''}`;
    infoMain.appendChild(replyLink);
    const replyAction = document.createElement('span');
    replyAction.className = 'h-threads-info-reply-btn xdex-history-reply-label';
    const replyActionLink = document.createElement('a');
    replyActionLink.className = 'xdex-history-reply-action';
    replyActionLink.href = historyReplyActionUrl;
    replyActionLink.target = '_blank';
    replyActionLink.rel = 'noopener';
    replyActionLink.textContent = '回应';
    replyAction.appendChild(document.createTextNode('['));
    replyAction.appendChild(replyActionLink);
    replyAction.appendChild(document.createTextNode(']'));
    infoMain.appendChild(replyAction);
    const deleteButton = document.createElement('button');
    deleteButton.type = 'button';
    deleteButton.className = 'xdex-history-delete';
    deleteButton.dataset.historyKey = result.key;
    deleteButton.title = '删除';
    deleteButton.innerHTML = XDEX_SVG_X;
    main.appendChild(deleteButton);
    // 回收站标识: 该串原始浏览数据存于回收站时展示, 点击跳转定位(墓碑数据层就绪后自动生效)
    // 已恢复(revivedAt)的墓碑不算「在回收站中」——与 getThreadHistoryTombstoneList 的过滤条件保持一致
    if (typeof getThreadHistoryStore === 'function') {
      const tomb = (getThreadHistoryStore().tombstones || {})[result.key];
      if (tomb && !tomb.purged && !tomb.revivedAt) {
        const mark = document.createElement('button');
        mark.type = 'button';
        mark.className = 'xdex-history-tombstone-mark';
        mark.title = `历史浏览数据在回收站中 · ${formatTombstoneRemainText(tomb.deletedAt, THREAD_HISTORY_TOMBSTONE_TTL_MS)}`;
        mark.setAttribute('aria-label', mark.title);
        mark.innerHTML = XDEX_SVG_TRASH;
        mark.addEventListener('click', function (e) {
          e.preventDefault();
          e.stopPropagation();
          openRecycleBinAndLocate(result.key);
        });
        main.appendChild(mark);
      }
    }
    if (item.imageFile) {
      const imageLink = document.createElement('a');
      imageLink.className = 'h-threads-img-a xdex-history-image';
      imageLink.href = buildThreadHistoryImageUrl(item.imageFile, true);
      imageLink.dataset.historyQuoteId = item.threadId || '';
      imageLink.target = '_blank';
      imageLink.rel = 'noopener';
      const img = document.createElement('img');
      img.className = 'h-threads-img';
      img.src = buildThreadHistoryImageUrl(item.imageFile, false);
      img.alt = item.imageFile;
      imageLink.appendChild(img);
      main.appendChild(imageLink);
    }
    if (item.sageHtml) {
      const sageDiv = document.createElement('div');
      sageDiv.className = 'h-threads-tips uk-text-danger uk-text-bold';
      sageDiv.innerHTML = item.sageHtml;
      main.appendChild(sageDiv);
    }
        const content = document.createElement('div');
    content.className = 'h-threads-content';
    if (item.contentHtml) content.innerHTML = item.contentHtml;
    else content.textContent = item.contentText || item.excerpt || '';
    if (item.contentTruncated) {
      limitThreadHistoryContentText(content, THREAD_HISTORY_EXCERPT_LIMIT);
      appendThreadHistoryTruncationMarker(content);
    }
    main.appendChild(content);
    enhanceHistoryRenderedContent(content);
    const footer = document.createElement('div');
    footer.className = 'xdex-history-footer';
    appendThreadHistoryText(footer, 'span', 'xdex-history-time', formatThreadHistoryTime(item.lastVisitedAt));
    appendThreadHistoryText(footer, 'span', 'xdex-history-visit-count', `共访问 ${item.visitCount || 1} 次`);
    appendThreadHistoryText(footer, 'span', 'xdex-history-page', `串内最远:P${item.maxVisitedPage || item.page || 1}`);
    appendThreadHistoryText(footer, 'span', 'xdex-history-current-page', `最近查看:P${item.page || 1}`);
    if (item.mode === 'po') appendThreadHistoryText(footer, 'span', 'xdex-history-po-label', 'Po');
    main.appendChild(footer);
    enhanceHistoryRenderedContent(footer);
    markAllCookies(getHistoryMarkedGroups(), wrapper);
    return wrapper;
  }
  const HISTORY_RENDER_INITIAL_COUNT = 24;
  const HISTORY_RENDER_BATCH_SIZE = 20;
  const HISTORY_RENDER_BATCH_THRESHOLD = 400;
  const historyRenderQueues = new Map();
  function findHistoryScrollContainer(element) {
    let el = element;
    while (el && el !== document.body && el !== document.documentElement) {
      const style = window.getComputedStyle(el);
      if (style.overflowY === 'auto' || style.overflowY === 'scroll' || style.overflow === 'auto' || style.overflow === 'scroll') {
        return el;
      }
      el = el.parentElement;
    }
    return element;
  }
  function cancelHistoryRenderQueue(queueId) {
    const prev = historyRenderQueues.get(queueId);
    if (!prev) return;
    prev.cancelled = true;
    if (prev.scrollHandler && prev.scrollContainer) {
      prev.scrollContainer.removeEventListener('scroll', prev.scrollHandler, { passive: true });
    }
    historyRenderQueues.delete(queueId);
  }
  function batchRenderHistoryItems(root, results, buildFn, queueId) {
    // 新批次顶替旧批次: 同队列已有在途渲染一律取消
    cancelHistoryRenderQueue(queueId);
    if (!root) return;
    const total = results.length;
    if (total <= 0) return;
    let cursor = 0;
    const state = { cancelled: false };
    const scrollContainer = findHistoryScrollContainer(root);
    historyRenderQueues.set(queueId, state);
    function appendBatch(count) {
      if (state.cancelled) return;
      const slice = results.slice(cursor, cursor + count);
      const fragment = document.createDocumentFragment();
      slice.forEach(r => fragment.appendChild(buildFn(r)));
      root.appendChild(fragment);
      cursor += slice.length;
    }
    function maybeLoadMore() {
      if (state.cancelled || cursor >= total) return;
      const nearBottom = scrollContainer.scrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - HISTORY_RENDER_BATCH_THRESHOLD;
      if (nearBottom) {
        appendBatch(HISTORY_RENDER_BATCH_SIZE);
        requestAnimationFrame(() => {
          if (!state.cancelled) maybeLoadMore();
        });
      }
    }
    const scrollHandler = () => {
      if (state.cancelled) return;
      if (cursor >= total) return;
      requestAnimationFrame(() => {
        if (state.cancelled) return;
        maybeLoadMore();
      });
    };
    state.scrollHandler = scrollHandler;
    state.scrollContainer = scrollContainer;
    scrollContainer.addEventListener('scroll', scrollHandler, { passive: true });
    appendBatch(HISTORY_RENDER_INITIAL_COUNT);
    if (cursor < total) {
      requestAnimationFrame(() => {
        if (state.cancelled) return;
        maybeLoadMore();
      });
    }
  }
  // ===== 共享 SVG 图标(统一视觉,替代字符"×"等占位) =====
  const XDEX_SVG_X = '<svg viewBox="0 0 24 24" style="display:block;width:11px;height:11px;margin:auto;" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"></path></svg>';
  const XDEX_SVG_TRASH = '<svg viewBox="0 0 24 24" style="display:block;width:13px;height:13px;" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
  // 分组行拖拽手柄图标(六点:2 列 × 3 行,点距紧凑,参考常见拖拽手柄样式)
  const XDEX_SVG_GRIP = '<svg viewBox="0 0 10 16" style="display:block;width:10px;height:16px;margin:auto;" fill="currentColor" aria-hidden="true"><circle cx="2.5" cy="3" r="1.3"></circle><circle cx="7.5" cy="3" r="1.3"></circle><circle cx="2.5" cy="8" r="1.3"></circle><circle cx="7.5" cy="8" r="1.3"></circle><circle cx="2.5" cy="13" r="1.3"></circle><circle cx="7.5" cy="13" r="1.3"></circle></svg>';
  // 回复模式行按钮图标(16px,与刷新饼干等 xdex-icon-btn 图标同尺寸)
  const XDEX_SVG_X_ICON = '<svg viewBox="0 0 24 24" style="display:block;width:16px;height:16px;" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"></path></svg>';
  const XDEX_SVG_STOPWATCH = '<svg viewBox="0 0 24 24" style="display:block;width:16px;height:16px;" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="13.6" r="7.4"></circle><path d="M12 13.6V9.8"></path><path d="M9.2 2.4h5.6"></path><path d="M12 2.4v3.8"></path><path d="M17.9 8.2l1.7-1.7"></path></svg>';
  const XDEX_SVG_INFINITY = '<svg viewBox="0 0 24 24" style="display:block;width:16px;height:16px;" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 12c-2-2.67-4-4-6-4a4 4 0 1 0 0 8c2 0 4-1.33 6-4Zm0 0c2 2.67 4 4 6 4a4 4 0 0 0 0-8c-2 0-4 1.33-6 4Z"></path></svg>';
  const XDEX_SVG_SWITCH = '<svg viewBox="0 0 24 24" style="display:block;width:16px;height:16px;" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m16 3 5 5-5 5"></path><path d="M21 8H5"></path><path d="m8 21-5-5 5-5"></path><path d="M3 16h16"></path></svg>';
  const XDEX_SVG_RESTORE_ICON = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 18A8.5 8.5 0 1 1 18.5 6.5"></path><path d="M18.5 6.5l-1.3 3.6"></path><path d="M19.2 10.6L18.5 6.5l-3.2 2.7"></path></svg>';
  const XDEX_SVG_PURGE_ICON = '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>';
  // 剩余时间略写: 24 小时内显示小时数, 超过显示天数
  function formatTombstoneRemainText(deletedAt, ttlMs) {
    const remain = Math.max(0, (Number(deletedAt) || 0) + (ttlMs || 0) - Date.now());
    const hours = Math.ceil(remain / (60 * 60 * 1000));
    return hours <= 24 ? `${hours} 小时后删除` : `${Math.ceil(remain / (24 * 60 * 60 * 1000))} 天后删除`;
  }
  const THREAD_HISTORY_TOMBSTONE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
  const THREAD_HISTORY_TOMBSTONE_HARD_TTL_MS = 180 * 24 * 60 * 60 * 1000;
  const POST_HISTORY_TOMBSTONE_HARD_TTL_MS = 180 * 24 * 60 * 60 * 1000;
  const POST_HISTORY_TOMBSTONE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
  // ===== 回收站 UI(墓碑数据层未接入前 tombstones 恒为空,视图显示空态) =====
  let threadHistoryRecycleMode = false;
  let threadHistoryRecycleSort = 'expiring'; // 'expiring'=即将删除优先 | 'recent'=最近删除优先
  function getThreadHistoryTombstoneList() {
    const store = getThreadHistoryStore();
    const tombs = store.tombstones || {};
    let arr = Object.keys(tombs)
      // 排除已彻底删除(purged)与已恢复(revivedAt)的墓碑:
      // 恢复后墓碑保留(revivedAt 供 WebDAV 传播、record 已清空)但不应再出现在回收站视图
      .filter((key) => !tombs[key].purged && !tombs[key].revivedAt)
      .map((key) => Object.assign({ key }, tombs[key]));
      const nowTs = Date.now();
      if (threadHistoryRecycleSort === 'due24h') {
        // 筛选: 剩余不足 24 小时的条目, 仍按即将删除优先排列
        arr = arr.filter(t => ((Number(t.deletedAt) || 0) + THREAD_HISTORY_TOMBSTONE_TTL_MS - nowTs) <= 24 * 60 * 60 * 1000);
        arr.sort((a, b) => (Number(a.deletedAt) || 0) - (Number(b.deletedAt) || 0));
      } else {
        arr.sort((a, b) => ((Number(a.deletedAt) || 0) - (Number(b.deletedAt) || 0)) * (threadHistoryRecycleSort === 'recent' ? -1 : 1));
      }
      return arr;
  }
  function openRecycleBinAndLocate(key) {
    setThreadHistoryRecycleMode(true);
    requestAnimationFrame(() => {
      const el = document.querySelector(`#sp_history_results [data-recycle-key="${CSS.escape(key)}"]`);
      if (!el) return;
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      el.classList.add('xdex-recycle-highlight');
      setTimeout(() => el.classList.remove('xdex-recycle-highlight'), 2000);
    });
  }
  function updateThreadHistoryRecycleBadge() {
    const badge = document.getElementById('sp_history_recycle_badge');
    if (!badge) return;
    const list = getThreadHistoryTombstoneList();
    // 角标仅在有条目"剩余不足 24 小时"(即将被彻底删除)时显示
    const dueCutoff = Date.now() + 24 * 60 * 60 * 1000;
    const dueSoonCount = list.filter(t => {
      const expireAt = (Number(t.deletedAt) || 0) + THREAD_HISTORY_TOMBSTONE_TTL_MS;
      return expireAt > Date.now() && expireAt <= dueCutoff;
    }).length;
    badge.textContent = String(dueSoonCount);
    badge.hidden = dueSoonCount === 0;
  }
  function renderThreadHistoryRecycleView() {
    const root = document.getElementById('sp_history_results');
    if (!root) return;
    const list = getThreadHistoryTombstoneList();
    const barCount = document.getElementById('sp_history_recycle_barcount');
    if (barCount) barCount.textContent = `${list.length} 条`;
    const emptyBtn = document.getElementById('sp_history_recycle_empty');
    if (emptyBtn) emptyBtn.disabled = list.length === 0;
    root.textContent = '';
    const now = Date.now();
    const liveStore = getThreadHistoryStore();
    for (const t of list) {
      const rec = t.record || {};
      // 删除后的新段(items 中被压制的独立计数)动态合成展示
      const shadow = liveStore.items[t.key] || null;
      const newVisits = shadow ? (Number(shadow.visitCount) || 0) : 0;
      // 合成展示视图: 快照为基础; 最近访问/最远页码与新段同步取最新, 与回收站信息保持一致
      const itemView = Object.assign({}, rec);
      if (shadow) {
        itemView.lastVisitedAt = Math.max(Number(rec.lastVisitedAt) || 0, Number(shadow.lastVisitedAt) || 0);
        itemView.maxVisitedPage = Math.max(Number(rec.maxVisitedPage) || 0, Number(shadow.maxVisitedPage) || 0);
        itemView.page = Math.max(Number(rec.page) || 0, Number(shadow.page) || 0);
      }
      // 复用正常条目构建器, 展示形式与主列表完全一致
      const card = buildThreadHistoryItemElement({
        key: t.key,
        item: itemView,
        index: buildThreadHistoryIndexEntry(itemView)
      });
      const legacyDelete = card.querySelector('.xdex-history-delete');
      if (legacyDelete) legacyDelete.remove();
      const legacyMark = card.querySelector('.xdex-history-tombstone-mark');
      if (legacyMark) legacyMark.remove();
      const item = document.createElement('div');
      item.className = 'xdex-recycle-item';
      item.dataset.recycleKey = t.key;
      item.appendChild(card);
      const metaEl = document.createElement('div');
      metaEl.className = 'xdex-recycle-item-meta';
      const metaText = document.createElement('span');
      metaText.textContent = `删除于 ${new Date(t.deletedAt).toLocaleString('zh-CN', { hour12: false })} · ${formatTombstoneRemainText(t.deletedAt, THREAD_HISTORY_TOMBSTONE_TTL_MS)}${newVisits > 0 ? ` · 新增访问 ${newVisits} 次` : ''}`;
      metaEl.appendChild(metaText);
      const restoreBtn = document.createElement('button');
      restoreBtn.type = 'button';
      restoreBtn.className = 'xdex-recycle-item-btn xdex-recycle-item-restore';
      restoreBtn.dataset.recycleKey = t.key;
      restoreBtn.title = '恢复该条目到浏览历史';
      restoreBtn.setAttribute('aria-label', '恢复');
      restoreBtn.innerHTML = XDEX_SVG_RESTORE_ICON;
      const purgeBtn = document.createElement('button');
      purgeBtn.type = 'button';
      purgeBtn.className = 'xdex-recycle-item-btn xdex-recycle-item-purge';
      purgeBtn.dataset.recycleKey = t.key;
      purgeBtn.title = '彻底删除(无法恢复)';
      purgeBtn.setAttribute('aria-label', '彻底删除');
      purgeBtn.innerHTML = XDEX_SVG_PURGE_ICON;
      metaEl.appendChild(restoreBtn);
      metaEl.appendChild(purgeBtn);
      item.appendChild(metaEl);
      root.appendChild(item);
    }
    if (!list.length) {
      const empty = document.createElement('div');
      empty.className = 'xdex-recycle-empty';
      empty.innerHTML = '回收站为空<br><span style="font-size:12px;opacity:.8;">已删除的浏览记录会在这里保留 30 天,期间可随时恢复;到期未恢复将自动彻底清除</span>';
      root.appendChild(empty);
    }
  }
  function setThreadHistoryRecycleMode(on) {
    const next = !!on;
    if (threadHistoryRecycleMode === next) return;
    threadHistoryRecycleMode = next;
    // 进入回收站前终止主列表在途批次: 否则旧批次回调会在回收站视图上继续追加普通条目
    if (next) cancelHistoryRenderQueue('threadHistory');
    const content = document.getElementById('sp_history_content');
    if (content) content.classList.toggle('xdex-recycle-mode', threadHistoryRecycleMode);
    const bar = document.getElementById('sp_history_recycle_bar');
    if (bar) bar.hidden = !threadHistoryRecycleMode;
    if (threadHistoryRecycleMode) {
      renderThreadHistoryRecycleView();
    } else {
      renderThreadHistoryModule();
    }
  }
  function renderThreadHistoryModule(query) {
    if (threadHistoryRecycleMode) { renderThreadHistoryRecycleView(); return; }
    updateThreadHistoryRecycleBadge();
    const root = document.getElementById('sp_history_results');
    if (!root) {
      logThreadHistory('render skipped: missing #sp_history_results');
      return;
    }
    invalidateHistoryRenderCaches();
    const input = document.getElementById('sp_history_search');
    const sortSelect = document.getElementById('sp_history_sort');
    const effectiveQuery = query == null && input ? input.value : query;
    const sortMode = sortSelect ? sortSelect.value : 'last-desc';
    const results = searchThreadHistory(effectiveQuery || '', null, sortMode);
    updateThreadHistoryDebugState({ lastRender: { query: effectiveQuery || '', sortMode, count: results.length, at: new Date().toISOString() } });
    logThreadHistory('render module', { query: effectiveQuery || '', sortMode, count: results.length });
    const count = document.getElementById('sp_history_count');
    if (count) {
      count.textContent = `${results.length} 条 `;
      count.appendChild(buildHistorySearchHelpMark(THREAD_HISTORY_SEARCH_HELP_TEXT));
    }
    root.textContent = '';
    if (!results.length) {
      const empty = document.createElement('div');
      empty.className = 'xdex-history-empty';
      empty.textContent = effectiveQuery ? '没有匹配的浏览历史' : '暂无浏览历史';
      root.appendChild(empty);
      return;
    }
    batchRenderHistoryItems(root, results, buildThreadHistoryItemElement, 'threadHistory');
    const reportThreadHistoryRenderDom = () => {
      const cover = document.getElementById('sp_cover');
      const panel = document.getElementById('sp_panel');
      const views = document.getElementById('sp_panel_views');
      const module = document.getElementById('sp_module_history');
      const panelContent = document.querySelector('#sp_module_history .sp_panel_content');
      const historyContent = document.getElementById('sp_history_content');
      const firstItem = root.querySelector('.xdex-history-item');
      updateThreadHistoryDebugState({
        lastRenderDom: {
          activeModule: module?.classList.contains('active') || false,
          contentDisplay: getComputedStyle(historyContent || root).display,
          resultsDisplay: getComputedStyle(root).display,
          coverDisplay: getComputedStyle(cover || document.body).display,
          panelDisplay: getComputedStyle(panel || document.body).display,
          viewsDisplay: getComputedStyle(views || document.body).display,
          moduleDisplay: getComputedStyle(module || document.body).display,
          panelContentDisplay: getComputedStyle(panelContent || document.body).display,
          childCount: root.children.length,
          itemCount: root.querySelectorAll('.xdex-history-item').length,
          coverHeight: cover?.offsetHeight || 0,
          panelHeight: panel?.offsetHeight || 0,
          viewsHeight: views?.offsetHeight || 0,
          moduleHeight: module?.offsetHeight || 0,
          panelContentHeight: panelContent?.offsetHeight || 0,
          historyContentHeight: historyContent?.offsetHeight || 0,
          offsetHeight: root.offsetHeight,
          scrollHeight: root.scrollHeight,
          firstItemHeight: firstItem?.offsetHeight || 0,
          firstItemText: firstItem?.textContent?.slice(0, 80) || '',
          at: new Date().toISOString()
        }
      });
      logThreadHistory('render dom', threadHistoryDebugState.lastRenderDom);
      logThreadHistoryFlat('render dom flat', threadHistoryDebugState.lastRenderDom);
    };
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(reportThreadHistoryRenderDom);
    else setTimeout(reportThreadHistoryRenderDom, 0);
  }
  function renderThreadHistoryModuleSoon(query) {
    if (typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(() => renderThreadHistoryModule(query));
      return;
    }
    setTimeout(() => renderThreadHistoryModule(query), 0);
  }
  function openHistoryImageQuotePreview(tid) {
    const quoteId = String(tid || '').trim();
    if (!/^\d+$/.test(quoteId) || quoteId === '9999999') return false;
    try {
      // 由调用方写入的“当前串”上下文(保持函数签名契约不变)
      const ctxThreadId = window.__xdexPendingQuoteCtxTid || ((typeof PageType !== 'undefined' && PageType.getThreadId) ? PageType.getThreadId(true) : '');
      window.__xdexPendingQuoteCtxTid = '';
      if (typeof window.__xdexOpenQuoteByTid !== 'function' && typeof enableQuotePreview === 'function') {
        enableQuotePreview();
      }
      if (typeof window.__xdexOpenQuoteByTid !== 'function') return false;
      const ret = window.__xdexOpenQuoteByTid(quoteId, { fromPOImage: true });
      if (ret && typeof ret.then === 'function') ret.catch(() => {});
      return true;
    } catch (e) {
      return false;
    }
  }
  let historyEnhanceCfgCache = null;
  let historyMarkedGroupsCache = null;
  function invalidateHistoryRenderCaches() {
    historyEnhanceCfgCache = null;
    historyMarkedGroupsCache = null;
  }
  function getHistoryEnhanceCfg() {
    if (historyEnhanceCfgCache) return historyEnhanceCfgCache;
    try {
      historyEnhanceCfgCache = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
    } catch (e) {
      historyEnhanceCfgCache = Object.assign({}, (SettingPanel && SettingPanel.defaults) || {});
    }
    return historyEnhanceCfgCache;
  }
  function getHistoryMarkedGroups() {
    if (historyMarkedGroupsCache) return historyMarkedGroupsCache;
    try {
      const cfg = typeof getFilterConfig === 'function' ? getFilterConfig() : getHistoryEnhanceCfg();
      historyMarkedGroupsCache = (cfg && cfg.markedGroups) || [];
    } catch (e) {
      historyMarkedGroupsCache = [];
    }
    return historyMarkedGroupsCache;
  }
  function enhanceHistoryRenderedContent(root) {
    if (!root) return;
    try { renderHiddenTextContent(root); } catch (e) {}
    try { if (typeof extendQuote === 'function') extendQuote(root); } catch (e) {}
    try { if (typeof initExtendedContent === 'function') initExtendedContent(root); } catch (e) {}
    try {
      const cfg = getHistoryEnhanceCfg();
      if (cfg && cfg.enableImageHideMode) applyImageHideMode(cfg.applyImageHideMode || 'default', root);
      if (cfg && cfg.enableAutoUrlLinkify && typeof runAutoUrlLinkify === 'function') runAutoUrlLinkify(root);
    } catch (e) {}
  }
  // 清空历史二次确认:首层自定义弹窗(可高亮「清空」「无法恢复」),确认后再走 window.confirm
  function showClearHistoryWarningDialog(onConfirm, onCancel) {
    const existing = document.getElementById('xdex-clear-history-warning-modal');
    if (existing) existing.remove();
    const $m = $(
      '<div id="xdex-clear-history-warning-modal" tabindex="-1" style="position:fixed;inset:0;z-index:10003;display:flex;align-items:center;justify-content:center;outline:none;">' +
        '<div class="xdex-clear-history-warning-backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,.45);"></div>' +
        '<div class="xdex-clear-history-warning-dialog" role="dialog" aria-modal="true" aria-labelledby="xdex-clear-history-warning-title" style="position:relative;width:min(380px,92vw);background:var(--xdex-sp-panel-bg, #FFFFEE);border:1px solid var(--xdex-sp-border, #ccc);border-radius:8px;box-shadow:0 2px 12px var(--xdex-sp-shadow, rgba(0,0,0,.25));padding:16px 18px 14px;color:var(--foreground, #333);">' +
          '<h3 id="xdex-clear-history-warning-title" style="margin:0 0 10px;font-size:16px;font-weight:700;color:#c62828;">操作警告</h3>' +
          '<p style="margin:0 0 8px;font-size:14px;line-height:1.65;">请注意此按钮为' +
            '<span style="color:#c62828;font-weight:800;font-size:16px;padding:0 2px;">「清空」</span>' +
            '按钮而非「搜索」</p>' +
          '<p style="margin:0 0 12px;font-size:14px;line-height:1.65;">操作后将' +
            '<span style="color:#c62828;font-weight:800;font-size:16px;padding:0 2px;">无法恢复</span></p>' +
          '<p style="margin:0 0 14px;font-size:13px;color:var(--muted-foreground, #666);">是否确认继续?</p>' +
          '<div style="display:flex;gap:8px;justify-content:flex-end;">' +
            '<button type="button" id="xdex-clear-history-warning-cancel" style="padding:6px 14px;">取消</button>' +
            '<button type="button" id="xdex-clear-history-warning-ok" style="padding:6px 14px;background:#c62828;color:#fff;border:1px solid #b71c1c;border-radius:4px;font-weight:700;">确认清空</button>' +
          '</div>' +
        '</div>' +
      '</div>'
    );
    function close() {
      $m.remove();
      $(document).off('keydown.xdex-clear-history-warning');
    }
    function cancel() {
      close();
      if (typeof onCancel === 'function') onCancel();
    }
    function confirm() {
      close();
      if (typeof onConfirm === 'function') onConfirm();
    }
    $m.find('.xdex-clear-history-warning-backdrop').on('click', cancel);
    $m.find('#xdex-clear-history-warning-cancel').on('click', cancel);
    $m.find('#xdex-clear-history-warning-ok').on('click', confirm);
    $(document).on('keydown.xdex-clear-history-warning', function (e) {
      if (e.key === 'Escape') {
        e.preventDefault();
        cancel();
      }
      // 危险操作:不响应 Enter 确认,必须点红色「确认清空」
    });
    $('body').append($m);
    try { $m.find('#xdex-clear-history-warning-cancel')[0].focus(); } catch (e) {}
  }
  // 检索输入统一绑定:拼音等输入法合成期间不检索,合成结束或停止输入后延迟一次,
  // 避免半截关键词反复触发全表扫描;回车立即检索并取消在途定时器
  function bindHistorySearchInput(selector, render, namespace) {
    const el = $(selector)[0];
    if (!el) return;
    if (el.__xdexSearchTimer) { clearTimeout(el.__xdexSearchTimer); el.__xdexSearchTimer = 0; }
    const schedule = (value) => {
      if (el.__xdexSearchTimer) clearTimeout(el.__xdexSearchTimer);
      el.__xdexSearchTimer = setTimeout(() => {
        el.__xdexSearchTimer = 0;
        if (document.contains(el)) render(value);
      }, HISTORY_SEARCH_INPUT_DEBOUNCE_MS);
    };
    $(el)
      .off('.' + namespace)
      .on('compositionstart.' + namespace, () => { el.__xdexSearchComposing = true; })
      .on('compositionend.' + namespace, (e) => {
        el.__xdexSearchComposing = false;
        schedule(e.target.value || '');
      })
      .on('input.' + namespace, function () {
        if (el.__xdexSearchComposing) return;
        schedule(this.value || '');
      })
      .on('keydown.' + namespace, function (e) {
        if (e.key !== 'Enter') return;
        if (el.__xdexSearchTimer) { clearTimeout(el.__xdexSearchTimer); el.__xdexSearchTimer = 0; }
        render(this.value || '');
      });
  }
  function bindThreadHistoryModuleEvents() {
    bindHistorySearchInput('#sp_history_search', renderThreadHistoryModule, 'xdex-history');
    $('#sp_history_sort').off('change.xdex-history').on('change.xdex-history', function () {
      renderThreadHistoryModule();
    });
    $('#sp_history_results').off('click.xdex-history-reply', '.xdex-history-reply-action').on('click.xdex-history-reply', '.xdex-history-reply-action', function (e) {
      if (e.button !== 0) return;
      const url = this.href || '';
      if (!url) return;
      e.preventDefault();
      if (e.ctrlKey || e.metaKey) {
        window.location.href = url;
        return;
      }
      window.open(url, '_blank', 'noopener');
    });
    $('#sp_history_results').off('click.xdex-history-image-quote', '.xdex-history-image').on('click.xdex-history-image-quote', '.xdex-history-image', function (e) {
      if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
      window.__xdexPendingQuoteCtxTid = (window.__xdexQuoteRefMark && window.__xdexQuoteRefMark.getContextThreadId(this)) || '';
      const opened = openHistoryImageQuotePreview(this.dataset.historyQuoteId || '');
      if (!opened) return;
      e.preventDefault();
      e.stopPropagation();
      if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
    });
    $('#sp_history_results').off('click.xdex-history-delete', '.xdex-history-delete').on('click.xdex-history-delete', '.xdex-history-delete', function (e) {
      e.preventDefault();
      const key = this.dataset.historyKey || '';
      if (!key) return;
      deleteThreadHistoryItem(key);
      renderThreadHistoryModule();
      toast('已移入回收站,30 天后自动清除,可在回收站恢复');
    });
    $('#sp_history_clear').off('click.xdex-history').on('click.xdex-history', function (e) {
      e.preventDefault();
      showClearHistoryWarningDialog(function () {
        if (!window.confirm('确定要清空全部浏览历史吗?')) return;
        clearThreadHistory();
        renderThreadHistoryModule();
        toast('已清空浏览历史(已移入回收站,30 天内可恢复)');
      });
    });
    $('#sp_history_recycle').off('click.xdex-history-recycle').on('click.xdex-history-recycle', function (e) {
      e.preventDefault();
      setThreadHistoryRecycleMode(true);
    });
    $('#sp_history_recycle_back').off('click.xdex-history-recycle').on('click.xdex-history-recycle', function (e) {
      e.preventDefault();
      setThreadHistoryRecycleMode(false);
    });
    $('#sp_history_recycle_sort').off('change.xdex-history-recycle').on('change.xdex-history-recycle', function () {
      threadHistoryRecycleSort = this.value;
      renderThreadHistoryRecycleView();
    });
    $('#sp_history_results').off('click.xdex-history-recycle-action', '.xdex-recycle-item-restore').on('click.xdex-history-recycle-action', '.xdex-recycle-item-restore', function (e) {
      e.preventDefault();
      e.stopPropagation();
      const key = this.dataset.recycleKey || '';
      if (!key) return;
      restoreThreadHistoryFromTombstone(key);
      renderThreadHistoryRecycleView();
      toast('已恢复该浏览记录');
    });
    $('#sp_history_results').off('click.xdex-history-recycle-action', '.xdex-recycle-item-purge').on('click.xdex-history-recycle-action', '.xdex-recycle-item-purge', function (e) {
      e.preventDefault();
      e.stopPropagation();
      const key = this.dataset.recycleKey || '';
      if (!key) return;
      if (!window.confirm('确定要彻底删除这条记录吗?此操作无法通过恢复找回。')) return;
      purgeThreadHistoryTombstone(key);
      renderThreadHistoryRecycleView();
      toast('已彻底删除');
    });
    $('#sp_history_recycle_empty').off('click.xdex-history-recycle-empty').on('click.xdex-history-recycle-empty', function (e) {
      e.preventDefault();
      if (this.disabled) return;
      if (!window.confirm('确定要清空回收站吗?全部待删除记录将被彻底清除,无法恢复。')) return;
      purgeAllThreadHistoryTombstones();
      renderThreadHistoryRecycleView();
      toast('回收站已清空');
    });
  }
  function buildPostHistoryItemElement(result) {
    const item = result.item || {};
    const wrapper = document.createElement('div');
    wrapper.className = 'xdex-history-item xdex-post-history-item';
    wrapper.dataset.postHistoryKey = result.key;
    const main = document.createElement('div');
    main.className = 'h-threads-item-main';
    wrapper.appendChild(main);
    const info = document.createElement('div');
    info.className = 'h-threads-info xdex-history-info xdex-post-history-info';
    main.appendChild(info);
    const infoMain = document.createElement('span');
    infoMain.className = 'xdex-history-info-main';
    info.appendChild(infoMain);
    if (shouldRenderThreadHistoryTitle(item.title)) appendThreadHistoryInfoText(infoMain, 'h-threads-info-title', item.title);
    if (shouldRenderThreadHistoryAuthor(item.name)) appendThreadHistoryInfoText(infoMain, 'h-threads-info-email', item.name);
    if (item.email) appendThreadHistoryInfoText(infoMain, 'h-threads-info-email', item.email);
    const submittedAtText = formatRelativeTimeMachineTime(item.submittedAt);
    const createdAtNode = appendThreadHistoryInfoText(infoMain, 'h-threads-info-createdat', submittedAtText);
    if (createdAtNode) {
      createdAtNode.dataset.xdexOriginalTime = submittedAtText;
      createdAtNode.title = submittedAtText;
    }
    if (item.userHash) appendThreadHistoryInfoText(infoMain, 'h-threads-info-uid', `ID:${item.userHash}`);
    const displayPostId = item.postId || item.id || (item.type === 'reply' ? '' : item.threadId);
    const postUrl = buildPostHistoryUrl(item.type, displayPostId, item.resto || item.threadId);
    const postReplyActionUrl = buildPostHistoryReplyActionUrl(item.type, displayPostId, item.resto || item.threadId, item.page);
    if (postUrl) {
      const postLink = document.createElement('a');
      postLink.className = 'h-threads-info-id xdex-post-history-thread-id';
      postLink.href = postUrl;
      postLink.textContent = `No.${displayPostId}`;
      infoMain.appendChild(postLink);
      const replyAction = document.createElement('span');
      replyAction.className = 'h-threads-info-reply-btn xdex-post-history-reply-label';
      const replyActionLink = document.createElement('a');
      replyActionLink.className = 'xdex-post-history-reply-action';
      replyActionLink.href = postReplyActionUrl;
      replyActionLink.target = '_blank';
      replyActionLink.rel = 'noopener';
      replyActionLink.textContent = '回应';
      replyAction.appendChild(document.createTextNode('['));
      replyAction.appendChild(replyActionLink);
      replyAction.appendChild(document.createTextNode(']'));
      infoMain.appendChild(replyAction);
    } else {
      appendThreadHistoryInfoText(infoMain, 'h-threads-info-id xdex-post-history-thread-id', '未确认');
    }
    const deleteButton = document.createElement('button');
    deleteButton.type = 'button';
    deleteButton.className = 'xdex-post-history-delete';
    deleteButton.dataset.postHistoryKey = result.key;
    deleteButton.title = '删除';
    deleteButton.innerHTML = XDEX_SVG_X;
    main.appendChild(deleteButton);
    // 回收站标识: 该发言原始数据存于回收站时展示, 点击跳转定位(墓碑数据层就绪后自动生效)
    // 已恢复(revivedAt)的墓碑不算「在回收站中」——与 getPostHistoryTombstoneList 的过滤条件保持一致
    if (typeof getPostHistoryStore === 'function') {
      const tomb = (getPostHistoryStore().tombstones || {})[result.key];
      if (tomb && !tomb.purged && !tomb.revivedAt) {
        const mark = document.createElement('button');
        mark.type = 'button';
        mark.className = 'xdex-post-history-tombstone-mark';
        mark.title = `历史发言数据在回收站中 · ${formatTombstoneRemainText(tomb.deletedAt, POST_HISTORY_TOMBSTONE_TTL_MS)}`;
        mark.setAttribute('aria-label', mark.title);
        mark.innerHTML = XDEX_SVG_TRASH;
        mark.addEventListener('click', function (e) {
          e.preventDefault();
          e.stopPropagation();
          openPostRecycleBinAndLocate(result.key);
        });
        main.appendChild(mark);
      }
    }
    if (item.imageFile) {
      const imageLink = document.createElement('a');
      imageLink.className = 'h-threads-img-a xdex-post-history-image';
      imageLink.href = buildThreadHistoryImageUrl(item.imageFile, true);
      imageLink.dataset.postHistoryQuoteId = displayPostId || item.threadId || '';
      imageLink.target = '_blank';
      imageLink.rel = 'noopener';
      const image = document.createElement('img');
      image.className = 'h-threads-img';
      image.src = buildThreadHistoryImageUrl(item.imageFile, false);
      image.alt = item.imageFile;
      imageLink.appendChild(image);
      main.appendChild(imageLink);
    }
    const content = document.createElement('div');
    content.className = 'h-threads-content';
    if (item.contentHtml) content.innerHTML = item.contentHtml;
    else content.textContent = item.contentText || item.contentRaw || '';
    main.appendChild(content);
    enhanceHistoryRenderedContent(content);
    const footer = document.createElement('div');
    footer.className = 'xdex-history-footer xdex-post-history-footer';
    if (item.status !== 'confirmed') appendThreadHistoryText(footer, 'span', 'xdex-post-history-status', item.status === 'pending' ? '确认中' : item.status === 'failed' ? '失败' : '未确认');
    const forumName = item.forumName || getPostHistoryForumNameByFid(item.fid);
    if (forumName) appendThreadHistoryText(footer, 'span', 'xdex-post-history-forum', `${forumName}`);
    appendThreadHistoryText(footer, 'span', 'xdex-post-history-type', item.type === 'reply' ? '回复' : '主题');
    if (item.threadId) appendThreadHistoryText(footer, 'span', 'xdex-post-history-thread', `串号:${item.threadId}`);
    // 优先从浏览历史获取最近浏览页(仅 thread 类型),否则回退到记录中的 page
    // const displayPage = item.page;
    let displayPage = item.page;
    if (item.type === 'thread' && item.threadId) {
      const historyUrl = getLatestThreadHistoryUrl(item.threadId);
      if (historyUrl) {
        const parsed = parseThreadHistoryUrl(historyUrl);
        if (parsed && parsed.page) displayPage = parsed.page;
      }
    }
    if (displayPage) appendThreadHistoryText(footer, 'span', 'xdex-post-history-page', `所在页:P${displayPage}`);
    main.appendChild(footer);
    enhanceHistoryRenderedContent(footer);
    markAllCookies(getHistoryMarkedGroups(), wrapper);
    return wrapper;
  }
  // ===== 发言历史回收站 UI(与浏览历史同构) =====
  let postHistoryRecycleMode = false;
  let postHistoryRecycleSort = 'expiring';
  function getPostHistoryTombstoneList() {
    const store = getPostHistoryStore();
    const tombs = store.tombstones || {};
    let arr = Object.keys(tombs)
      // 排除已彻底删除(purged)与已恢复(revivedAt)的墓碑:
      // 恢复后墓碑保留(revivedAt 供 WebDAV 传播、record 已清空)但不应再出现在回收站视图
      .filter((key) => !tombs[key].purged && !tombs[key].revivedAt)
      .map((key) => Object.assign({ key }, tombs[key]));
      const nowTs = Date.now();
      if (postHistoryRecycleSort === 'due24h') {
        arr = arr.filter(t => ((Number(t.deletedAt) || 0) + POST_HISTORY_TOMBSTONE_TTL_MS - nowTs) <= 24 * 60 * 60 * 1000);
        arr.sort((a, b) => (Number(a.deletedAt) || 0) - (Number(b.deletedAt) || 0));
      } else {
        arr.sort((a, b) => ((Number(a.deletedAt) || 0) - (Number(b.deletedAt) || 0)) * (postHistoryRecycleSort === 'recent' ? -1 : 1));
      }
      return arr;
  }
  function openPostRecycleBinAndLocate(key) {
    setPostHistoryRecycleMode(true);
    requestAnimationFrame(() => {
      const el = document.querySelector(`#sp_posts_results [data-recycle-key="${CSS.escape(key)}"]`);
      if (!el) return;
      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      el.classList.add('xdex-recycle-highlight');
      setTimeout(() => el.classList.remove('xdex-recycle-highlight'), 2000);
    });
  }
  function updatePostHistoryRecycleBadge() {
    const badge = document.getElementById('sp_posts_recycle_badge');
    if (!badge) return;
    const list = getPostHistoryTombstoneList();
    // 角标仅在有条目"剩余不足 24 小时"(即将被彻底删除)时显示
    const dueCutoff = Date.now() + 24 * 60 * 60 * 1000;
    const dueSoonCount = list.filter(t => {
      const expireAt = (Number(t.deletedAt) || 0) + POST_HISTORY_TOMBSTONE_TTL_MS;
      return expireAt > Date.now() && expireAt <= dueCutoff;
    }).length;
    badge.textContent = String(dueSoonCount);
    badge.hidden = dueSoonCount === 0;
  }
  function renderPostHistoryRecycleView() {
    const root = document.getElementById('sp_posts_results');
    if (!root) return;
    const list = getPostHistoryTombstoneList();
    const barCount = document.getElementById('sp_posts_recycle_barcount');
    if (barCount) barCount.textContent = `${list.length} 条`;
    const emptyBtn = document.getElementById('sp_posts_recycle_empty');
    if (emptyBtn) emptyBtn.disabled = list.length === 0;
    root.textContent = '';
    const now = Date.now();
    const liveStore = getPostHistoryStore();
    for (const t of list) {
      const rec = t.record || {};
      // 删除后的新记录(items 中被压制的独立条目)动态合成展示
      const shadow = liveStore.items[t.key] || null;
      // 合成展示视图: 快照为基础; 页码/时间与新记录同步取最新
      const itemView = Object.assign({}, rec);
      if (shadow) {
        itemView.submittedAt = Math.min(Number(rec.submittedAt) || Infinity, Number(shadow.submittedAt) || Infinity);
        itemView.page = Math.max(Number(rec.page) || 0, Number(shadow.page) || 0);
        itemView.contentText = shadow.contentText || rec.contentText;
      }
      // 复用正常条目构建器, 展示形式与发言历史主列表完全一致
      const card = buildPostHistoryItemElement({
        key: t.key,
        item: itemView
      });
      const legacyDelete = card.querySelector('.xdex-post-history-delete');
      if (legacyDelete) legacyDelete.remove();
      const legacyMark = card.querySelector('.xdex-post-history-tombstone-mark');
      if (legacyMark) legacyMark.remove();
      const item = document.createElement('div');
      item.className = 'xdex-recycle-item';
      item.dataset.recycleKey = t.key;
      item.appendChild(card);
      const metaEl = document.createElement('div');
      metaEl.className = 'xdex-recycle-item-meta';
      const metaText = document.createElement('span');
      metaText.textContent = `删除于 ${new Date(t.deletedAt).toLocaleString('zh-CN', { hour12: false })} · ${formatTombstoneRemainText(t.deletedAt, POST_HISTORY_TOMBSTONE_TTL_MS)}${shadow ? ' · 删除后有新记录' : ''}`;
      metaEl.appendChild(metaText);
      const restoreBtn = document.createElement('button');
      restoreBtn.type = 'button';
      restoreBtn.className = 'xdex-recycle-item-btn xdex-recycle-item-restore';
      restoreBtn.dataset.recycleKey = t.key;
      restoreBtn.title = '恢复该条目到发言历史';
      restoreBtn.setAttribute('aria-label', '恢复');
      restoreBtn.innerHTML = XDEX_SVG_RESTORE_ICON;
      const purgeBtn = document.createElement('button');
      purgeBtn.type = 'button';
      purgeBtn.className = 'xdex-recycle-item-btn xdex-recycle-item-purge';
      purgeBtn.dataset.recycleKey = t.key;
      purgeBtn.title = '彻底删除(无法恢复)';
      purgeBtn.setAttribute('aria-label', '彻底删除');
      purgeBtn.innerHTML = XDEX_SVG_PURGE_ICON;
      metaEl.appendChild(restoreBtn);
      metaEl.appendChild(purgeBtn);
      item.appendChild(metaEl);
      root.appendChild(item);
    }
    if (!list.length) {
      const empty = document.createElement('div');
      empty.className = 'xdex-recycle-empty';
      empty.innerHTML = '回收站为空<br><span style="font-size:12px;opacity:.8;">已删除的发言记录会在这里保留 30 天,期间可随时恢复;到期未恢复将自动彻底清除</span>';
      root.appendChild(empty);
    }
  }
  function setPostHistoryRecycleMode(on) {
    const next = !!on;
    if (postHistoryRecycleMode === next) return;
    postHistoryRecycleMode = next;
    // 进入回收站前终止主列表在途批次
    if (next) cancelHistoryRenderQueue('postHistory');
    const content = document.getElementById('sp_posts_content');
    if (content) content.classList.toggle('xdex-recycle-mode', postHistoryRecycleMode);
    const bar = document.getElementById('sp_posts_recycle_bar');
    if (bar) bar.hidden = !postHistoryRecycleMode;
    if (postHistoryRecycleMode) {
      renderPostHistoryRecycleView();
    } else {
      renderPostHistoryModule();
    }
  }
  function renderPostHistoryModule(query) {
    if (postHistoryRecycleMode) { renderPostHistoryRecycleView(); return; }
    updatePostHistoryRecycleBadge();
    const root = document.getElementById('sp_posts_results');
    if (!root) return;
    postHistoryLiveRenderDirty = false;
    invalidateHistoryRenderCaches();
    const input = document.getElementById('sp_posts_search');
    const effectiveQuery = query == null && input ? input.value : query;
    const results = searchPostHistory(effectiveQuery || '', postHistoryActiveType);
    const count = document.getElementById('sp_posts_count');
    if (count) {
      count.textContent = `${results.length} 条 `;
      count.appendChild(buildHistorySearchHelpMark(POST_HISTORY_SEARCH_HELP_TEXT));
    }
    root.textContent = '';
    if (!results.length) {
      const empty = document.createElement('div');
      empty.className = 'xdex-history-empty xdex-post-history-empty';
      empty.textContent = effectiveQuery ? '没有匹配的我的发言' : (postHistoryActiveType === 'reply' ? '暂无我的回复' : '暂无我的主题');
      root.appendChild(empty);
      return;
    }
    batchRenderHistoryItems(root, results, buildPostHistoryItemElement, 'postHistory');
  }
  function renderPostHistoryModuleSoon(query) {
    postHistoryLiveRenderDirty = false;
    if (typeof requestAnimationFrame === 'function') {
      requestAnimationFrame(() => renderPostHistoryModule(query));
      return;
    }
    setTimeout(() => renderPostHistoryModule(query), 0);
  }
  // ─── 订阅 Feed 渲染 ──────────────────────────────────────────────────────
  const SUBSCRIPTION_FEED_API_BASE = 'https://api.nmb.best/api';
  const SUBSCRIPTION_FEED_SESSION_STORE_KEY = 'xdex_subscription_feed_sessions_v3';
  const SUBSCRIPTION_FEED_SESSION_VERSION = 3;
  const SUBSCRIPTION_FEED_SESSION_TTL_MS = 2 * 60 * 1000;
  const SUBSCRIPTION_FEED_SESSION_MAX_COUNT = 5;
  let subscriptionFeedCurrentPage = 1;
  let subscriptionFeedCurrentUuid = '';
  let subscriptionFeedLoading = false;
  let subscriptionFeedAllItems = [];
  let subscriptionFeedHasMore = true;
  let subscriptionFeedRequestSeq = 0;
  const subscriptionFeedInflightPages = Object.create(null);
  let subscriptionFeedRenderedPages = Object.create(null);
  let subscriptionFeedHighestRenderedPage = 0;
  let subscriptionFeedCurrentDisplayPage = 1;
  let subscriptionFeedCacheExpired = false;
  const ACTIVE_FEED_STORAGE_KEY = 'xdex_active_subscription_feed_uuid';
  function getActiveSubscriptionFeedUuid() {
    try { return GM_getValue(ACTIVE_FEED_STORAGE_KEY, ''); } catch (e) { return ''; }
  }
  function setActiveSubscriptionFeedUuid(uuid) {
    try { GM_setValue(ACTIVE_FEED_STORAGE_KEY, uuid || ''); } catch (e) {}
  }
  function createDefaultSubscriptionFeedSessionStore() {
    return {
      version: SUBSCRIPTION_FEED_SESSION_VERSION,
      sessions: {}
    };
  }
  function normalizeSubscriptionFeedSessionStore(rawStore) {
    const store = Object.assign(createDefaultSubscriptionFeedSessionStore(), rawStore || {});
    store.version = SUBSCRIPTION_FEED_SESSION_VERSION;
    store.sessions = store.sessions && typeof store.sessions === 'object' ? store.sessions : {};
    return store;
  }
  function getSubscriptionFeedSessionStore() {
    try {
      return normalizeSubscriptionFeedSessionStore(GM_getValue(SUBSCRIPTION_FEED_SESSION_STORE_KEY, null));
    } catch (e) {
      return createDefaultSubscriptionFeedSessionStore();
    }
  }
  function setSubscriptionFeedSessionStore(store) {
    const normalized = normalizeSubscriptionFeedSessionStore(store);
    GM_setValue(SUBSCRIPTION_FEED_SESSION_STORE_KEY, normalized);
    return normalized;
  }
  function getSubscriptionFeedItemId(item) {
    return String(Number(item && item.id) || 0);
  }
  function normalizeSubscriptionFeedPageItems(items) {
    const list = Array.isArray(items) ? items : [];
    const seen = new Set();
    return list.filter((item) => {
      const id = getSubscriptionFeedItemId(item);
      if (!id || id === '0' || seen.has(id)) return false;
      seen.add(id);
      return true;
    });
  }
  function buildSubscriptionFeedPageEntry(page, items) {
    const normalizedPage = Math.max(1, Number(page) || 1);
    const normalizedItems = normalizeSubscriptionFeedPageItems(items);
    return {
      page: normalizedPage,
      fetchedAt: Date.now(),
      itemIds: normalizedItems.map(getSubscriptionFeedItemId),
      items: normalizedItems
    };
  }
  function computeHighestContiguousSubscriptionFeedPage(pages) {
    let page = 1;
    while (pages && pages[String(page)] && Number(pages[String(page)].page) === page) page++;
    return page - 1;
  }
  function normalizeSubscriptionFeedSession(session) {
    if (!session || typeof session !== 'object') return null;
    const uuid = String(session.uuid || '').trim();
    if (!uuid) return null;
    const cachedPages = session.cachedPages && typeof session.cachedPages === 'object' ? session.cachedPages : {};
    const normalizedPages = {};
    Object.keys(cachedPages).forEach((key) => {
      const entry = cachedPages[key] || {};
      const page = Math.max(1, Number(entry.page || key) || 1);
      const normalizedItems = normalizeSubscriptionFeedPageItems(entry.items);
      normalizedPages[String(page)] = {
        page,
        fetchedAt: Number(entry.fetchedAt) || 0,
        itemIds: normalizedItems.map(getSubscriptionFeedItemId),
        items: normalizedItems
      };
    });
    const highestCachedPage = computeHighestContiguousSubscriptionFeedPage(normalizedPages);
    const createdAt = Number(session.createdAt) || 0;
    const expiresAt = Number(session.expiresAt) || 0;
    const lastAccessAt = Number(session.lastAccessAt) || createdAt || Date.now();
    return {
      version: SUBSCRIPTION_FEED_SESSION_VERSION,
      uuid,
      createdAt,
      expiresAt,
      lastAccessAt,
      highestCachedPage,
      cachedPages: normalizedPages
    };
  }
  function isSubscriptionFeedSessionExpired(session) {
    return !session || !session.expiresAt || Date.now() > Number(session.expiresAt);
  }
  function isSubscriptionFeedSessionStructurallyValid(session) {
    const normalized = normalizeSubscriptionFeedSession(session);
    if (!normalized) return false;
    if (!normalized.createdAt || !normalized.expiresAt) return false;
    for (let page = 1; page <= normalized.highestCachedPage; page++) {
      const entry = normalized.cachedPages[String(page)];
      if (!entry || Number(entry.page) !== page || !Array.isArray(entry.items)) return false;
    }
    return true;
  }
  function getSubscriptionFeedSession(uuid) {
    const key = String(uuid || '').trim();
    if (!key) return null;
    const store = getSubscriptionFeedSessionStore();
    return normalizeSubscriptionFeedSession(store.sessions[key]);
  }
  function saveSubscriptionFeedSession(session) {
    const normalized = normalizeSubscriptionFeedSession(session);
    if (!normalized) return null;
    const store = getSubscriptionFeedSessionStore();
    store.sessions[normalized.uuid] = normalized;
    const ordered = Object.values(store.sessions)
      .map(normalizeSubscriptionFeedSession)
      .filter(Boolean)
      .sort((a, b) => (Number(b.lastAccessAt) || 0) - (Number(a.lastAccessAt) || 0));
    const limited = ordered.slice(0, SUBSCRIPTION_FEED_SESSION_MAX_COUNT);
    store.sessions = {};
    limited.forEach((item) => {
      store.sessions[item.uuid] = item;
    });
    setSubscriptionFeedSessionStore(store);
    return normalized;
  }
  function deleteSubscriptionFeedSession(uuid) {
    const key = String(uuid || '').trim();
    if (!key) return;
    const store = getSubscriptionFeedSessionStore();
    if (!store.sessions[key]) return;
    delete store.sessions[key];
    setSubscriptionFeedSessionStore(store);
  }
  function pruneExpiredSubscriptionFeedSessions() {
    const store = getSubscriptionFeedSessionStore();
    let changed = false;
    Object.keys(store.sessions).forEach((uuid) => {
      const session = normalizeSubscriptionFeedSession(store.sessions[uuid]);
      if (!session || isSubscriptionFeedSessionExpired(session) || !isSubscriptionFeedSessionStructurallyValid(session)) {
        delete store.sessions[uuid];
        changed = true;
        return;
      }
      store.sessions[uuid] = session;
    });
    if (changed) setSubscriptionFeedSessionStore(store);
  }
  function createSubscriptionFeedSession(uuid, firstPageEntry) {
    const now = Date.now();
    const normalizedUuid = String(uuid || '').trim();
    const cachedPages = {};
    let highestCachedPage = 0;
    if (firstPageEntry && Array.isArray(firstPageEntry.items) && firstPageEntry.items.length) {
      cachedPages[String(firstPageEntry.page)] = firstPageEntry;
      highestCachedPage = firstPageEntry.page === 1 ? 1 : 0;
    }
    return {
      version: SUBSCRIPTION_FEED_SESSION_VERSION,
      uuid: normalizedUuid,
      createdAt: now,
      expiresAt: now + SUBSCRIPTION_FEED_SESSION_TTL_MS,
      lastAccessAt: now,
      highestCachedPage,
      cachedPages
    };
  }
  function flattenSubscriptionFeedSessionItems(session) {
    const normalized = normalizeSubscriptionFeedSession(session);
    if (!normalized || normalized.highestCachedPage <= 0) return [];
    const items = [];
    for (let page = 1; page <= normalized.highestCachedPage; page++) {
      const entry = normalized.cachedPages[String(page)];
      if (!entry || !Array.isArray(entry.items)) break;
      items.push(...entry.items);
    }
    return items;
  }
  function resetSubscriptionFeedRuntimeState(uuid) {
    subscriptionFeedCurrentUuid = String(uuid || '').trim();
    subscriptionFeedCurrentPage = 0;
    subscriptionFeedAllItems = [];
    subscriptionFeedHasMore = true;
    subscriptionFeedRenderedPages = Object.create(null);
    subscriptionFeedHighestRenderedPage = 0;
    subscriptionFeedCurrentDisplayPage = 1;
    subscriptionFeedCacheExpired = false;
  }
  function rebuildSubscriptionFeedRuntimeList() {
    const items = [];
    for (let page = 1; page <= subscriptionFeedHighestRenderedPage; page++) {
      const entry = subscriptionFeedRenderedPages[String(page)];
      if (!entry || !Array.isArray(entry.items)) break;
      items.push(...entry.items);
    }
    subscriptionFeedAllItems = items;
    subscriptionFeedCurrentPage = subscriptionFeedHighestRenderedPage;
  }
  function applySubscriptionFeedSessionToRuntime(session) {
    const normalized = normalizeSubscriptionFeedSession(session);
    if (!normalized) return false;
    subscriptionFeedCurrentUuid = normalized.uuid;
    subscriptionFeedRenderedPages = Object.create(null);
    for (let page = 1; page <= normalized.highestCachedPage; page++) {
      const entry = normalized.cachedPages[String(page)];
      if (!entry) break;
      subscriptionFeedRenderedPages[String(page)] = entry;
    }
    subscriptionFeedHighestRenderedPage = normalized.highestCachedPage;
    subscriptionFeedCurrentDisplayPage = normalized.highestCachedPage > 0 ? 1 : 0;
    rebuildSubscriptionFeedRuntimeList();
    subscriptionFeedHasMore = true;
    subscriptionFeedCacheExpired = false;
    return true;
  }
  function appendSubscriptionFeedRenderedPage(pageEntry) {
    if (!pageEntry || !pageEntry.page || !Array.isArray(pageEntry.items)) return false;
    const page = Math.max(1, Number(pageEntry.page) || 1);
    if (page !== subscriptionFeedHighestRenderedPage + 1) return false;
    subscriptionFeedRenderedPages[String(page)] = pageEntry;
    subscriptionFeedHighestRenderedPage = page;
    subscriptionFeedCurrentDisplayPage = page;
    rebuildSubscriptionFeedRuntimeList();
    return true;
  }
  function getSubscriptionFeedNextRenderPage() {
    return Math.max(0, Number(subscriptionFeedHighestRenderedPage) || 0) + 1;
  }
  function appendSubscriptionFeedPageSeparator(root, page) {
    if (!root || !page || page <= 1) return;
    const sep = document.createElement('div');
    sep.className = 'xdex-feed-page-separator';
    sep.style.cssText = 'text-align:center;color:#999;font-size:12px;padding:8px 0;border-top:1px dashed #ddd;margin-top:8px;';
    sep.textContent = `——第${page}页——`;
    root.appendChild(sep);
  }
  function updateSubscriptionFeedDisplayPageFromScroll() {
    const container = document.querySelector('#sp_module_feeds .sp_panel_content');
    const results = document.getElementById('sp_feeds_results');
    if (!container || !results || subscriptionFeedHighestRenderedPage <= 0) return;
    const separators = Array.from(results.querySelectorAll('.xdex-feed-page-separator'));
    let displayPage = 1;
    const threshold = container.scrollTop + container.clientHeight / 2;
    separators.forEach((sep) => {
      const page = Number((sep.textContent || '').match(/第(\d+)页/)?.[1] || 0);
      if (!page) return;
      const top = sep.offsetTop;
      if (threshold >= top) displayPage = page;
    });
    subscriptionFeedCurrentDisplayPage = Math.min(Math.max(1, displayPage), Math.max(1, subscriptionFeedHighestRenderedPage));
    $('#sp_feeds_page_label').text(`第${subscriptionFeedCurrentDisplayPage}页`);
  }
  function renderSubscriptionFeedRenderedPages(options = {}) {
    const $results = $('#sp_feeds_results').empty();
    const displayPage = Math.max(0, Number(options.displayPage) || 0) || subscriptionFeedCurrentDisplayPage || subscriptionFeedHighestRenderedPage || 0;
    if (subscriptionFeedHighestRenderedPage <= 0) {
      $results.html('<div style="text-align:center;color:#999;padding:40px 0;">暂无订阅内容</div>');
      $('#sp_feeds_page_label').text('第0页');
      return;
    }
    for (let page = 1; page <= subscriptionFeedHighestRenderedPage; page++) {
      const entry = subscriptionFeedRenderedPages[String(page)];
      if (!entry || !Array.isArray(entry.items)) break;
      appendSubscriptionFeedPageSeparator($results[0], page);
      entry.items.forEach((item) => {
        $results[0].appendChild(buildSubscriptionFeedItemElement(item));
      });
    }
    subscriptionFeedCurrentDisplayPage = Math.min(Math.max(1, displayPage), Math.max(1, subscriptionFeedHighestRenderedPage));
    $('#sp_feeds_page_label').text(`第${subscriptionFeedCurrentDisplayPage}页`);
    if (typeof requestAnimationFrame === 'function') requestAnimationFrame(updateSubscriptionFeedDisplayPageFromScroll);
    else setTimeout(updateSubscriptionFeedDisplayPageFromScroll, 0);
  }
  function restoreSubscriptionFeedSession(uuid) {
    const session = getSubscriptionFeedSession(uuid);
    if (!session || isSubscriptionFeedSessionExpired(session) || !isSubscriptionFeedSessionStructurallyValid(session)) {
      if (session) deleteSubscriptionFeedSession(uuid);
      return false;
    }
    const restored = Object.assign({}, session, { lastAccessAt: Date.now() });
    saveSubscriptionFeedSession(restored);
    applySubscriptionFeedSessionToRuntime(restored);
    renderSubscriptionFeedRenderedPages({ displayPage: 1 });
    return true;
  }
  function invalidateSubscriptionFeedSession(uuid, reason) {
    const key = String(uuid || '').trim();
    if (!key) return;
    console.info('[subscription-feed] invalidate session', { uuid: key, reason: reason || '' });
    deleteSubscriptionFeedSession(key);
  }
  function populateSubscriptionFeedSelector() {
    const $sel = $('#sp_feeds_selector').empty();
    const $display = $('#sp_feeds_selector_display');
    const $dropdown = $('#sp_feeds_selector_dropdown').empty().hide();
    const $desc = $display.find('.xdex-feed-display-desc');
    const $uuid = $display.find('.xdex-feed-display-uuid');
    const feeds = (typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [];
    if (!feeds.length) {
      $sel.append('<option value="">(请先添加订阅号)</option>');
      $desc.text('请先在设置中添加订阅号');
      $uuid.text('');
      return '';
    }
    feeds.forEach((f, i) => {
      const label = f.desc ? `${f.desc}:${f.uuid}` : f.uuid;
      $sel.append(`<option value="${Utils.escapeHTML ? Utils.escapeHTML(f.uuid) : f.uuid}">${Utils.escapeHTML ? Utils.escapeHTML(label) : label}</option>`);
      const $opt = $('<div class="xdex-feed-option" role="option"></div>')
        .attr('data-uuid', f.uuid)
        .text(label);
      $dropdown.append($opt);
    });
    const saved = getActiveSubscriptionFeedUuid();
    const match = feeds.find(f => f.uuid === saved);
    const selected = match ? match.uuid : feeds[0].uuid;
    $sel.val(selected);
    const activeFeed = feeds.find(f => f.uuid === selected);
    $desc.text(activeFeed && activeFeed.desc ? activeFeed.desc : '');
    // 无备注时也要显示订阅号(打码),避免选择框看起来是空的
    $uuid.text(activeFeed && activeFeed.uuid ? activeFeed.uuid : '');
    $uuid.toggleClass('xdex-feed-uuid-light-blur', !!(activeFeed && activeFeed.uuid && !activeFeed.desc));
    $dropdown.find('.xdex-feed-option').removeClass('active').filter(`[data-uuid="${selected}"]`).addClass('active');
    return selected;
  }
  function buildSubscriptionFeedItemElement(item) {
    const threadId = Number(item.id) || 0;
    const wrapper = document.createElement('div');
    wrapper.className = 'xdex-history-item';
    wrapper.dataset.feedThreadId = String(threadId);
    const main = document.createElement('div');
    main.className = 'h-threads-item-main';
    wrapper.appendChild(main);
    const info = document.createElement('div');
    info.className = 'h-threads-info xdex-history-info';
    main.appendChild(info);
    const infoMain = document.createElement('span');
    infoMain.className = 'xdex-history-info-main';
    info.appendChild(infoMain);
    const title = String(item.title || '');
    const email = String(item.email || '');
    const now = String(item.now || '');
    const userHash = String(item.user_hash || '');
    if (title && title !== '无标题') appendThreadHistoryInfoText(infoMain, 'h-threads-info-title', title);
    if (email) appendThreadHistoryInfoText(infoMain, 'h-threads-info-email', email);
    const createdAtNode = appendThreadHistoryInfoText(infoMain, 'h-threads-info-createdat', now);
    if (createdAtNode) {
      createdAtNode.dataset.xdexOriginalTime = now;
      createdAtNode.title = now;
    }
    if (userHash) {
      const cookieSpan = appendThreadHistoryText(infoMain, 'span', 'h-threads-info-uid', `ID:${userHash}`);
      if (cookieSpan) cookieSpan.setAttribute('data-xdex-cookie-id', userHash);
    }
    const replyLink = document.createElement('a');
    replyLink.className = 'h-threads-info-id xdex-history-thread-id';
    replyLink.href = `${location.origin}/t/${threadId}`;
    replyLink.textContent = `No.${threadId}`;
    infoMain.appendChild(replyLink);
    const replyCount = Number(item.reply_count) || 0;
    if (replyCount > 0) {
      appendThreadHistoryText(infoMain, 'span', 'xdex-history-visit-count', `${replyCount} 回`);
    }
    // 从浏览历史查找最近查看页
    const tid = String(threadId || '').trim();
    const histStore = getThreadHistoryStore();
    const histCandidates = ['normal', 'po']
      .map((mode) => histStore.items[getThreadHistoryKey(mode, tid)])
      .filter(Boolean)
      .sort((a, b) => (Number(b.lastVisitedAt) || 0) - (Number(a.lastVisitedAt) || 0));
    const histItem = histCandidates[0] || null;
    const histPage = histItem ? (Number(histItem.page) || 1) : 1;
    // [回应] 链接
    const replyAction = document.createElement('span');
    replyAction.className = 'h-threads-info-reply-btn xdex-history-reply-label';
    const replyActionLink = document.createElement('a');
    replyActionLink.className = 'xdex-history-reply-action';
    replyActionLink.href = `${location.origin}/t/${tid}?page=${histPage}`;
    replyActionLink.target = '_blank';
    replyActionLink.rel = 'noopener';
    replyActionLink.textContent = '回应';
    replyAction.appendChild(document.createTextNode('['));
    replyAction.appendChild(replyActionLink);
    replyAction.appendChild(document.createTextNode(']'));
    infoMain.appendChild(replyAction);
    // 取消订阅按钮
    const deleteButton = document.createElement('button');
    deleteButton.type = 'button';
    deleteButton.className = 'xdex-post-history-delete';
    deleteButton.dataset.feedThreadId = String(threadId);
    deleteButton.title = '取消订阅';
    deleteButton.innerHTML = XDEX_SVG_X;
    main.appendChild(deleteButton);
    // 图片
    const imgRaw = String(item.img || '');
    const extRaw = String(item.ext || '');
    if (imgRaw) {
      const suffix = extRaw ? (extRaw[0] === '.' ? extRaw : `.${extRaw}`) : '';
      const imageFile = suffix && imgRaw.toLowerCase().endsWith(suffix.toLowerCase()) ? imgRaw : imgRaw + suffix;
      const imageLink = document.createElement('a');
      imageLink.className = 'h-threads-img-a xdex-history-image';
      imageLink.dataset.historyQuoteId = threadId;
      imageLink.href = buildThreadHistoryImageUrl(imageFile, true);
      imageLink.target = '_blank';
      imageLink.rel = 'noopener';
      const img = document.createElement('img');
      img.className = 'h-threads-img';
      img.src = buildThreadHistoryImageUrl(imageFile, false);
      img.alt = imageFile;
      imageLink.appendChild(img);
      main.appendChild(imageLink);
    }
    // 正文
    const content = document.createElement('div');
    content.className = 'h-threads-content';
    const contentHtml = String(item.content || '');
    if (contentHtml) content.innerHTML = contentHtml;
    else content.textContent = '';
    main.appendChild(content);
    enhanceHistoryRenderedContent(content);
    // 脚注
    const footer = document.createElement('div');
    footer.className = 'xdex-history-footer';
    const fid = String(item.fid || '');
    const forumName = fid ? (POST_HISTORY_FORUM_FID_MAP[fid] || '') : '';
    if (forumName) appendThreadHistoryText(footer, 'span', 'xdex-post-history-forum', forumName);
    appendThreadHistoryText(footer, 'span', 'xdex-post-history-type', '订阅');
    appendThreadHistoryText(footer, 'span', 'xdex-history-time', now);
    main.appendChild(footer);
    enhanceHistoryRenderedContent(footer);
    markAllCookies(getFilterConfig().markedGroups || [], wrapper);
    return wrapper;
  }
  async function fetchSubscriptionFeedPage(uuid, page) {
    const url = `${SUBSCRIPTION_FEED_API_BASE}/feed?uuid=${encodeURIComponent(uuid)}&page=${encodeURIComponent(page)}`;
    const resp = await gmRequest(url, 'json');
    const data = resp.response || resp.responseText;
    if (Array.isArray(data)) return data;
    try { return JSON.parse(typeof data === 'string' ? data : '[]'); } catch (e) { return []; }
  }
  function renderSubscriptionFeedModule() {
    const $results = $('#sp_feeds_results').empty();
    pruneExpiredSubscriptionFeedSessions();
    const uuid = populateSubscriptionFeedSelector();
    if (!uuid) {
      $results.html('<div style="text-align:center;color:#999;padding:40px 0;">请先在设置中添加订阅号</div>');
      subscriptionFeedCurrentUuid = '';
      subscriptionFeedHasMore = false;
      return;
    }
    subscriptionFeedCurrentUuid = uuid;
    if (restoreSubscriptionFeedSession(uuid)) return;
    resetSubscriptionFeedRuntimeState(uuid);
    $results.html('<div style="text-align:center;color:#999;padding:40px 0;">正在获取订阅……</div>');
    loadSubscriptionFeedPage(uuid, 1, { replace: true, source: 'init' });
  }
  async function loadSubscriptionFeedPage(uuid, page, options = {}) {
    const replace = !!options.replace;
    if (!uuid || subscriptionFeedLoading) return;
    const requestPage = Math.max(1, Number(page) || 1);
    const $results = $('#sp_feeds_results');
    if (replace && subscriptionFeedHighestRenderedPage <= 0) {
      resetSubscriptionFeedRuntimeState(uuid);
      if ($results.length) $results.empty();
    }
    const expectedPage = getSubscriptionFeedNextRenderPage();
    if (requestPage !== expectedPage) {
      console.warn('[subscription-feed] reject non-contiguous page', {
        uuid,
        page: requestPage,
        expectedPage,
        source: options.source || '',
        currentPage: subscriptionFeedCurrentPage,
        highestRenderedPage: subscriptionFeedHighestRenderedPage,
        allItems: subscriptionFeedAllItems.length,
        cacheExpired: subscriptionFeedCacheExpired
      });
      return;
    }
    const session = getSubscriptionFeedSession(uuid);
    if (session && isSubscriptionFeedSessionExpired(session)) subscriptionFeedCacheExpired = true;
    subscriptionFeedLoading = true;
    const seq = ++subscriptionFeedRequestSeq;
    subscriptionFeedInflightPages[requestPage] = seq;
    try {
      const items = normalizeSubscriptionFeedPageItems(await fetchSubscriptionFeedPage(uuid, requestPage));
      if (subscriptionFeedInflightPages[requestPage] !== seq) return;
      if (uuid !== subscriptionFeedCurrentUuid) return;
      if (!items.length) {
        subscriptionFeedHasMore = false;
        renderSubscriptionFeedRenderedPages({ displayPage: subscriptionFeedCurrentDisplayPage });
        return;
      }
      const pageEntry = buildSubscriptionFeedPageEntry(requestPage, items);
      if (!appendSubscriptionFeedRenderedPage(pageEntry)) return;
      subscriptionFeedHasMore = true;
      renderSubscriptionFeedRenderedPages({ displayPage: subscriptionFeedCurrentDisplayPage });
      if (!subscriptionFeedCacheExpired) {
        let nextSession = getSubscriptionFeedSession(uuid);
        if (!nextSession || isSubscriptionFeedSessionExpired(nextSession) || !isSubscriptionFeedSessionStructurallyValid(nextSession)) {
          nextSession = createSubscriptionFeedSession(uuid, pageEntry);
        } else {
          nextSession.cachedPages[String(requestPage)] = pageEntry;
          nextSession.highestCachedPage = computeHighestContiguousSubscriptionFeedPage(nextSession.cachedPages);
          nextSession.lastAccessAt = Date.now();
        }
        saveSubscriptionFeedSession(nextSession);
      }
    } catch (err) {
      console.error('[subscription-feed] load error', err);
      if (replace) {
        $results.html(`<div style="text-align:center;color:#c00;padding:40px 0;">加载失败:${Utils.escapeHTML ? Utils.escapeHTML(err.message) : err.message}</div>`);
      }
    } finally {
      if (subscriptionFeedInflightPages[requestPage] === seq) delete subscriptionFeedInflightPages[requestPage];
      subscriptionFeedLoading = false;
    }
  }
  function bindSubscriptionFeedModuleEvents() {
    // 订阅号切换
    // 自定义下拉菜单交互
    const $wrap = $('.xdex-feed-selector-wrap');
    const $display = $('#sp_feeds_selector_display');
    const $dropdown = $('#sp_feeds_selector_dropdown');
    // 鼠标悬停时临时显示 UUID,移出后恢复模糊(比 CSS :hover 更可靠)
    const $uuid = $display.find('.xdex-feed-display-uuid');
    $wrap.off('mouseenter.feedBlur mouseleave.feedBlur')
      .on('mouseenter.feedBlur', () => $uuid.css('filter', 'none'))
      .on('mouseleave.feedBlur', () => $uuid.css('filter', ''));
    $display.off('click.feedDropdown').on('click.feedDropdown', (e) => {
      e.stopPropagation();
      const isOpen = $dropdown.is(':visible');
      $dropdown.toggle(!isOpen);
      $display.attr('aria-expanded', String(!isOpen));
    });
    $dropdown.off('click.feedOption', '.xdex-feed-option').on('click.feedOption', '.xdex-feed-option', function (e) {
      e.stopPropagation();
      const uuid = $(this).data('uuid') || '';
      if (!uuid) return;
      $('#sp_feeds_selector').val(uuid).trigger('change.subscriptionFeed');
      const feeds = (typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [];
      const feed = feeds.find(f => f.uuid === uuid);
      const $uuidEl = $display.find('.xdex-feed-display-uuid');
      $display.find('.xdex-feed-display-desc').text(feed && feed.desc ? feed.desc : '');
      // 无备注时也显示打码订阅号
      $uuidEl.text(feed && feed.uuid ? feed.uuid : '');
      $uuidEl.toggleClass('xdex-feed-uuid-light-blur', !!(feed && feed.uuid && !feed.desc));
      $dropdown.find('.xdex-feed-option').removeClass('active').filter('[data-uuid="' + uuid + '"]').addClass('active');
      $dropdown.hide();
      $display.attr('aria-expanded', 'false');
    });
    $(document).off('click.feedDropdownClose').on('click.feedDropdownClose', () => {
      $dropdown.hide();
      $display.attr('aria-expanded', 'false');
    });
    $('#sp_feeds_selector').off('change.subscriptionFeed').on('change.subscriptionFeed', function () {
      subscriptionFeedCurrentUuid = $(this).val() || '';
      if (subscriptionFeedCurrentUuid) {
        setActiveSubscriptionFeedUuid(subscriptionFeedCurrentUuid);
        renderSubscriptionFeedModule();
      }
    });
    // 跨页面同步:其他标签页切换订阅号时自动刷新
    if (typeof GM_addValueChangeListener === 'function') {
      GM_addValueChangeListener(ACTIVE_FEED_STORAGE_KEY, (_key, _oldVal, newVal, remote) => {
        if (!remote) return;
        const uuid = String(newVal || '');
        if (uuid && uuid !== subscriptionFeedCurrentUuid) {
          subscriptionFeedCurrentUuid = uuid;
          $('#sp_feeds_selector').val(uuid);
          renderSubscriptionFeedModule();
        }
      });
    }
    // 跳转
    $('#sp_feeds_page_jump').off('click.subscriptionFeed').on('click.subscriptionFeed', (e) => {
      e.preventDefault();
      const page = parseInt($('#sp_feeds_page_input').val(), 10);
      if (!page || page < 1 || !subscriptionFeedCurrentUuid) return;
      const expectedPage = getSubscriptionFeedNextRenderPage();
      if (page !== expectedPage) {
        toast(`当前缓存会话仅支持连续翻页,请先加载第${expectedPage}页`);
        return;
      }
      loadSubscriptionFeedPage(subscriptionFeedCurrentUuid, page, { replace: false, source: 'jump' });
    });
    // 滚动加载下一页
    const $scrollContainer = $('#sp_module_feeds .sp_panel_content');
    $scrollContainer.off('scroll.subscriptionFeed').on('scroll.subscriptionFeed', function () {
      updateSubscriptionFeedDisplayPageFromScroll();
      if (subscriptionFeedLoading || !subscriptionFeedHasMore || !subscriptionFeedCurrentUuid) return;
      const el = this;
      if (el.scrollTop + el.clientHeight >= el.scrollHeight - 400) {
        const nextPage = getSubscriptionFeedNextRenderPage();
        loadSubscriptionFeedPage(subscriptionFeedCurrentUuid, nextPage, { replace: false, source: 'scroll' });
      }
    });
    // 上一页
    $('#sp_feeds_prev').off('click.subscriptionFeed').on('click.subscriptionFeed', (e) => {
      e.preventDefault();
      toast('当前缓存会话仅支持向后连续翻页');
    });
    // 下一页
    $('#sp_feeds_next').off('click.subscriptionFeed').on('click.subscriptionFeed', (e) => {
      e.preventDefault();
      if (!subscriptionFeedCurrentUuid) return;
      const nextPage = getSubscriptionFeedNextRenderPage();
      loadSubscriptionFeedPage(subscriptionFeedCurrentUuid, nextPage, { replace: false, source: 'next-button' });
    });
    // 订阅面板图片点击 → 打开引用弹窗(图片激活态)
    $('#sp_feeds_results').off('click.xdex-feed-image-quote', '.xdex-history-image').on('click.xdex-feed-image-quote', '.xdex-history-image', function (e) {
      if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
      window.__xdexPendingQuoteCtxTid = (window.__xdexQuoteRefMark && window.__xdexQuoteRefMark.getContextThreadId(this)) || '';
      const opened = openHistoryImageQuotePreview(this.dataset.historyQuoteId || '');
      if (!opened) return;
      e.preventDefault();
      e.stopPropagation();
      if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
    });
    // 取消订阅
    $('#sp_feeds_results').off('click.xdex-feed-delete', '.xdex-post-history-delete').on('click.xdex-feed-delete', '.xdex-post-history-delete', function (e) {
      e.preventDefault();
      const tid = this.dataset.feedThreadId || '';
      if (!tid || !subscriptionFeedCurrentUuid) return;
      if (!window.confirm('确定要取消订阅这个串吗?')) return;
      GM_xmlhttpRequest({
        method: 'POST',
        url: `${SUBSCRIPTION_FEED_API_BASE}/delFeed`,
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        data: `uuid=${encodeURIComponent(subscriptionFeedCurrentUuid)}&tid=${encodeURIComponent(tid)}`,
        onload: () => {
          invalidateSubscriptionFeedSession(subscriptionFeedCurrentUuid, 'delete-feed');
          resetSubscriptionFeedRuntimeState(subscriptionFeedCurrentUuid);
          toast('已取消订阅');
          renderSubscriptionFeedModule();
        },
          onerror: () => toast('取消订阅失败')
      });
    });
    // ── 批量添加订阅 ──
    $('#sp_feeds_bulk_add_input').attr('autocomplete', 'off');
    $('#sp_feeds_bulk_add_btn').off('click.subscriptionFeedBulkAdd').on('click.subscriptionFeedBulkAdd', (e) => {
      e.preventDefault();
      const uuid = subscriptionFeedCurrentUuid || resolveSubscriptionFeedUuid();
      if (!uuid) { toast('请先在设置中添加一个订阅号'); return; }
      const raw = ($('#sp_feeds_bulk_add_input').val() || '').trim();
      if (!raw) { toast('请输入串号'); return; }
      const tids = raw.split(/[,,\s]+/).map(s => s.replace(/^(?:No\.)/i, '').trim()).filter(s => /^\d{6,8}$/.test(s));
      if (!tids.length) { toast('未识别到有效串号'); return; }
      const $btn = $(e.currentTarget);
      $btn.prop('disabled', true).text('添加中…');
      let success = 0, fail = 0, idx = 0;
      const failedTids = [];
      const BATCH_DELAY = 300;
      function next() {
        if (idx >= tids.length) {
           $btn.prop('disabled', false).text('批量添加');
           invalidateSubscriptionFeedSession(uuid, 'bulk-add');
           renderSubscriptionFeedModule();
           toast(`批量添加完成:成功 ${success},失败 ${fail},共 ${tids.length} 条`);
           if (failedTids.length) {
             $('#sp_feeds_bulk_add_input').val(failedTids.join(','));
           } else {
             $('#sp_feeds_bulk_add_input').val('');
           }
          return;
        }
        const tid = tids[idx++];
        GM_xmlhttpRequest({
          method: 'POST',
          url: `${SUBSCRIPTION_FEED_API_BASE}/addFeed`,
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          data: `uuid=${encodeURIComponent(uuid)}&tid=${encodeURIComponent(tid)}`,
             onload: (resp) => {
                try {
                  const raw = String(resp.responseText || resp.response || '');
                  const msg = JSON.parse(raw);
                  if (typeof msg === 'string' && /不存在|失败|错误/.test(msg)) { fail++; failedTids.push(tid); }
                  else success++;
                } catch (_) {
                  success++;
                }
               setTimeout(next, BATCH_DELAY);
             },
           onerror: () => { fail++; failedTids.push(tid); setTimeout(next, BATCH_DELAY); }
        });
      }
      next();
    });
    // ── 导出串号到剪贴板 ──
    $('#sp_feeds_export_clipboard').off('click.subscriptionFeedExport').on('click.subscriptionFeedExport', (e) => {
      e.preventDefault();
      const uuid = subscriptionFeedCurrentUuid || resolveSubscriptionFeedUuid();
      if (!uuid) { toast('请先在设置中添加一个订阅号'); return; }
      const $btn = $(e.currentTarget);
      $btn.prop('disabled', true).text('导出中…');
      const allTids = [];
      let page = 1;
      const PAGE_DELAY = 500;
      function fetchNext() {
        fetchSubscriptionFeedPage(uuid, page).then(items => {
          if (!items || !items.length) {
            // 全部拉完
            $btn.prop('disabled', false).text('导出串号');
            if (!allTids.length) { toast('该订阅号下无订阅内容'); return; }
            const text = allTids.join(',');
            navigator.clipboard.writeText(text).then(() => {
              toast(`已导出 ${allTids.length} 个串号到剪贴板`);
            }).catch(() => {
              toast('导出失败,无法写入剪贴板');
            });
            return;
          }
          items.forEach(item => {
            const id = String(item.id || '').trim();
            if (id && id !== '0' && !allTids.includes(id)) allTids.push(id);
          });
          page++;
          setTimeout(fetchNext, PAGE_DELAY);
        }).catch(() => {
          $btn.prop('disabled', false).text('导出串号');
          toast('导出失败,网络错误');
        });
      }
      fetchNext();
    });
  }
  function setPostHistoryType(type) {
    postHistoryActiveType = normalizePostHistoryType(type);
    $('#sp_posts_type_buttons [data-post-history-type]').removeClass('active')
      .filter(`[data-post-history-type="${postHistoryActiveType}"]`).addClass('active');
    renderPostHistoryModule();
  }
  function bindPostHistoryModuleEvents() {
    bindHistorySearchInput('#sp_posts_search', renderPostHistoryModule, 'xdex-post-history');
    $('#sp_posts_type_buttons').off('click.xdex-post-history', '[data-post-history-type]').on('click.xdex-post-history', '[data-post-history-type]', function (e) {
      e.preventDefault();
      setPostHistoryType(this.dataset.postHistoryType || 'thread');
    });
    $('#sp_posts_results').off('click.xdex-post-history-reply', '.xdex-post-history-reply-action').on('click.xdex-post-history-reply', '.xdex-post-history-reply-action', function (e) {
      if (e.button !== 0) return;
      const url = this.href || '';
      if (!url) return;
      e.preventDefault();
      if (e.ctrlKey || e.metaKey) {
        window.location.href = url;
        return;
      }
      window.open(url, '_blank', 'noopener');
    });
    $('#sp_posts_results').off('click.xdex-post-history-image-quote', '.xdex-post-history-image').on('click.xdex-post-history-image-quote', '.xdex-post-history-image', function (e) {
      if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
      window.__xdexPendingQuoteCtxTid = (window.__xdexQuoteRefMark && window.__xdexQuoteRefMark.getContextThreadId(this)) || '';
      const opened = openHistoryImageQuotePreview(this.dataset.postHistoryQuoteId || '');
      if (!opened) return;
      e.preventDefault();
      e.stopPropagation();
      if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
    });
    $('#sp_posts_results').off('click.xdex-post-history-delete', '.xdex-post-history-delete').on('click.xdex-post-history-delete', '.xdex-post-history-delete', function (e) {
      e.preventDefault();
      const key = this.dataset.postHistoryKey || '';
      if (!key) return;
      if (!window.confirm('确定要删除这条发言记录吗?')) return;
      deletePostHistoryItem(key);
      renderPostHistoryModule();
      toast('已移入回收站,30 天后自动清除,可在回收站恢复');
    });
    $('#sp_posts_clear').off('click.xdex-post-history').on('click.xdex-post-history', function (e) {
      e.preventDefault();
      showClearHistoryWarningDialog(function () {
        if (!window.confirm('确定要清空全部我的发言记录吗?')) return;
        clearPostHistory();
        renderPostHistoryModule();
        toast('已清空我的发言(已移入回收站,30 天内可恢复)');
      });
    });
    $('#sp_posts_recycle').off('click.xdex-post-history-recycle').on('click.xdex-post-history-recycle', function (e) {
      e.preventDefault();
      setPostHistoryRecycleMode(true);
    });
    $('#sp_posts_recycle_back').off('click.xdex-post-history-recycle').on('click.xdex-post-history-recycle', function (e) {
      e.preventDefault();
      setPostHistoryRecycleMode(false);
    });
    $('#sp_posts_recycle_sort').off('change.xdex-post-history-recycle').on('change.xdex-post-history-recycle', function () {
      postHistoryRecycleSort = this.value;
      renderPostHistoryRecycleView();
    });
    $('#sp_posts_results').off('click.xdex-post-history-recycle-action', '.xdex-recycle-item-restore').on('click.xdex-post-history-recycle-action', '.xdex-recycle-item-restore', function (e) {
      e.preventDefault();
      e.stopPropagation();
      const key = this.dataset.recycleKey || '';
      if (!key) return;
      restorePostHistoryFromTombstone(key);
      renderPostHistoryRecycleView();
      toast('已恢复该发言记录');
    });
    $('#sp_posts_results').off('click.xdex-post-history-recycle-action', '.xdex-recycle-item-purge').on('click.xdex-post-history-recycle-action', '.xdex-recycle-item-purge', function (e) {
      e.preventDefault();
      e.stopPropagation();
      const key = this.dataset.recycleKey || '';
      if (!key) return;
      if (!window.confirm('确定要彻底删除这条记录吗?此操作无法通过恢复找回。')) return;
      purgePostHistoryTombstone(key);
      renderPostHistoryRecycleView();
      toast('已彻底删除');
    });
    $('#sp_posts_recycle_empty').off('click.xdex-post-history-recycle-empty').on('click.xdex-post-history-recycle-empty', function (e) {
      e.preventDefault();
      if (this.disabled) return;
      if (!window.confirm('确定要清空回收站吗?全部待删除记录将被彻底清除,无法恢复。')) return;
      purgeAllPostHistoryTombstones();
      renderPostHistoryRecycleView();
      toast('回收站已清空');
    });
    // 手动添加发言历史
    // 禁用浏览器自动填充
    $('#sp_posts_manual_add_input').attr('autocomplete', 'off');
    $('#sp_posts_manual_add_btn').off('click.xdex-post-history-manual').on('click.xdex-post-history-manual', function (e) {
      e.preventDefault();
      const raw = ($('#sp_posts_manual_add_input').val() || '').trim();
      // 支持格式:No.67024789、67024789、https://nmbxd1.com/t/67024789、67024789?r=68811442&page=23
      let postId = '';
      let replyId = '';
      let inputPage = 0;
      // 解析 URL 中的主题号、回复号、页码
      const urlMatch = raw.match(/(?:https?:\/\/[^/]*\/t\/)?(\d{6,8})(?:\?([^#]*))?(?:#.*)?$/i);
      const simpleMatch = raw.match(/^(?:No\.)?(\d{6,8})$/);
      if (urlMatch) {
        postId = urlMatch[1];
        const qs = urlMatch[2] || '';
        const rMatch = qs.match(/(?:^|&)r=(\d{6,8})(?:&|$)/);
        const pMatch = qs.match(/(?:^|&)page=(\d+)(?:&|$)/);
        replyId = rMatch ? rMatch[1] : '';
        inputPage = pMatch ? parseInt(pMatch[1], 10) : 0;
      } else if (simpleMatch) {
        postId = simpleMatch[1];
      } else {
        toast('格式错误,支持:No.67024789、67024789、https://nmbxd1.com/t/67024789、67024789?r=68811442&page=23');
        return;
      }
      // 前后号相同 → 直接当主题处理
      if (replyId && replyId === postId) replyId = '';
      const existingStore = getPostHistoryStore();
      // 查找是否已存在匹配记录:有 replyId 时只匹配回复号,无 replyId 时匹配主题号
      const existingMatch = replyId
        ? Object.values(existingStore.items).find(item =>
            String(item.id) === replyId || String(item.postId) === replyId
          )
        : Object.values(existingStore.items).find(item =>
            String(item.id) === postId || String(item.postId) === postId
          );
      // 已存在且有完整信息 → 检查是否需要补充页码
      if (existingMatch && existingMatch.resto && existingMatch.resto !== '0') {
        if (inputPage && inputPage !== Number(existingMatch.page || 0)) {
          // 有新页码且与已有记录不同 → 更新页码
          updatePostHistoryRecord(existingMatch.localId, { page: inputPage });
          renderPostHistoryModule();
          $('#sp_posts_manual_add_input').val('');
          toast('已更新 No.' + (replyId || postId) + ' 的页码为 ' + inputPage);
        } else {
          toast(replyId ? '回复 No.' + replyId + ' 已存在' : '主题 No.' + postId + ' 已存在');
        }
        return;
      }
      const $btn = $(this);
      $btn.prop('disabled', true).text('获取中…');
      // 有 replyId → 直接用 ref API 获取回复内容
      // 无 replyId → 用 ref API 获取帖子内容,再用 thread API 判定类型
      const refTarget = replyId || postId;
      const refUrl = 'https://api.nmb.best/api/ref?id=' + encodeURIComponent(refTarget);
      const refHeaders = getPostHistoryApiCookieHeaders();
      const refDetail = { id: refTarget, source: 'manual-add', api: true };
      gmRequest(refUrl, 'text', refHeaders).then(async (resp) => {
        const post = parsePostHistoryRefResponse(resp, refDetail);
        if (!post || !post.id) { toast('未找到该串/回复'); return; }
        let type = 'thread';
        let resto = '0';
        let fid = getPostHistoryPostFid(post);
        if (replyId) {
          // 有 replyId → 明确是回复,resto = 主题号
          type = 'reply';
          resto = postId;
          try {
            const pageData = await fetchPostHistoryThreadPage(postId, 1, { source: 'manual-add-fid' });
            if (!fid) fid = getPostHistoryPostFid(pageData && pageData.thread) || '';
          } catch (e) {}
        } else {
          // 无 replyId → 用 thread API 判定类型
          try {
            const pageData = await fetchPostHistoryThreadPage(post.id, 1, { source: 'manual-add-type' });
            const thread = pageData && pageData.thread;
            if (thread && thread.id && String(thread.id) === String(post.id)) {
              if (!fid) fid = getPostHistoryPostFid(thread) || '';
            } else {
              type = 'reply';
              toast('该串号为回复,无法确定所属主题');
            }
          } catch (e) {
            type = 'reply';
            toast('该串号为回复,无法确定所属主题');
          }
        }
        // 饼干校验
        const userHash = String(post.user_hash || post.userHash || '').trim();
        const cookieList = getCookiesList();
        const matchedCookie = userHash
          ? Object.values(cookieList).find(c => abbreviateName(c.name || '') === userHash)
          : null;
        if (userHash && !matchedCookie) {
          if (!window.confirm('No.' + (replyId || postId) + ' 对应的饼干 ' + userHash + ' 不在当前列表之中,是否确认添加?')) {
            $btn.prop('disabled', false).text('手动添加');
            return;
          }
        }
        const serverContentRaw = post.content || '';
        const serverContentText = normalizePostHistoryText(serverContentRaw);
        const imageFile = buildPostHistoryImageFile(post.img, post.ext);
        // 无主题号的回复 → url 留空,不构建跳转链接
        const url = (type === 'reply' && resto === '0') ? '' : buildPostHistoryUrl(type, replyId || post.id, resto);
        if (existingMatch) {
          // 已存在但 resto=0 → 补充真实串号和相关信息
          const patch = {};
          if (type === 'reply' && resto !== '0') {
            patch.resto = resto;
            patch.threadId = resto;
            patch.url = url;
          }
          if (inputPage && !existingMatch.page) {
            patch.page = inputPage;
          }
          if (fid && !existingMatch.fid) {
            patch.fid = fid;
            patch.forumName = getPostHistoryForumNameByFid(fid);
          }
          if (imageFile && !existingMatch.imageFile) {
            Object.assign(patch, { imageFile, imageImg: post.img || '', imageExt: post.ext || '' });
          }
          updatePostHistoryRecord(existingMatch.localId, patch);
          renderPostHistoryModule();
          $('#sp_posts_manual_add_input').val('');
          const updated = [];
          if (patch.resto) updated.push('串号');
          if (patch.page) updated.push('页码');
          if (patch.fid) updated.push('板块');
          if (patch.imageFile) updated.push('图片');
          toast('已更新 No.' + (replyId || postId) + ':' + (updated.join('、') || '无变化'));
        } else {
          // 新记录
          const localId = 'local-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
          const record = {
            status: 'confirmed',
            type,
            localId,
            id: post.id,
            resto,
            threadId: type === 'reply' ? resto : post.id,
            postId: post.id,
            page: inputPage || Number(post.page) || (type === 'thread' ? 1 : 0),
            fid,
            forumName: getPostHistoryForumNameByFid(fid),
            title: post.title || '',
            name: post.name || '',
            email: post.email || '',
            contentRaw: serverContentRaw,
            contentText: serverContentText,
            contentHash: hashPostHistoryText(serverContentText),
            contentHtml: sanitizePostHistoryServerContentHtml(serverContentRaw),
            userHash,
            submittedAt: Date.parse(post.now) || Date.now(),
            confirmedAt: Date.now(),
            sourceUrl: location.href,
            url
          };
          if (imageFile) Object.assign(record, { imageFile, imageImg: post.img || '', imageExt: post.ext || '' });
          upsertPostHistoryRecord(record);
          renderPostHistoryModule();
          $('#sp_posts_manual_add_input').val('');
          toast(type === 'thread' ? '已添加主题 No.' + postId : '已添加回复 No.' + (replyId || postId));
        }
      }).catch(() => {
        toast('获取失败,请检查串号或网络');
      }).finally(() => {
        $btn.prop('disabled', false).text('手动添加');
      });
    });
    $('#sp_posts_manual_add_input').off('keydown.xdex-post-history-manual').on('keydown.xdex-post-history-manual', function (e) {
      if (e.key === 'Enter') { e.preventDefault(); $('#sp_posts_manual_add_btn').trigger('click.xdex-post-history-manual'); }
    });
  }

  function getFavoriteThreadsConfig() {
    try {
      const cfg = Object.assign({}, SettingPanel.defaults, SettingPanel.state || {}, GM_getValue(SettingPanel.key, {}));
      cfg.favoriteThreads = normalizeFavoriteThreads(cfg.favoriteThreads);
      return cfg;
    } catch (e) {
      const cfg = Object.assign({}, SettingPanel.defaults, SettingPanel.state || {});
      cfg.favoriteThreads = normalizeFavoriteThreads(cfg.favoriteThreads);
      return cfg;
    }
  }
  function isFavoriteThreadPageLocation() {
    const path = window.location.pathname || '';
    return /^\/t\/\d{6,8}(?:\/\d+)?\/?$/.test(path)
      || /^\/Forum\/po\/id\/\d{6,8}(?:\/page\/\d+)?(?:\.html)?$/i.test(path);
  }
  function getCurrentFavoriteThreadId() {
    if (!isFavoriteThreadPageLocation()) return '';
    const fromUrl = normalizeFavoriteThreadInput(window.location.href);
    if (fromUrl) return fromUrl;
    const mainThreadId = document.querySelector('.h-threads-list .h-threads-item[data-threads-id]')?.getAttribute('data-threads-id') || '';
    return isValidThreadId(mainThreadId) ? mainThreadId : '';
  }
  function getFavoriteThreadsAddLinkText() {
    return isFavoriteThreadPageLocation() ? '添加当前串' : '添加常用串';
  }
  function getCurrentFavoriteThreadDesc() {
    const fromTitle = String(document.title || '').replace(/\s+-\s+No\.\d{6,8}.*$/, '').trim();
    const title = fromTitle && !/^No\.\d{6,8}/.test(fromTitle) ? fromTitle : selectEnhanceIslandTitleText();
    return trimFavoriteThreadDesc(title);
  }
  function openFavoriteThreadsSettingsPanel(options = {}) {
    try {
      if (!$('#sp_btn').length) SettingPanel.init();
      $('#sp_btn').trigger('click');
      window.setTimeout(() => {
        const $container = $('#favorite-thread-inputs-container');
        const $fold = $container.closest('.sp_fold');
        const $body = $fold.children('.sp_fold_body');
        if (!$body.is(':visible')) {
          $body.slideDown(150);
          $('#btn_sp_favoriteThreads,#btn_group_favoriteThreads').removeClass('xdex-btn-hidden');
        }
        if ($fold[0]) $fold[0].scrollIntoView({ block: 'center' });
        if (options.addEmptyGroup) {
          const nextIndex = $container.find('.favorite-thread-row').length + 1;
          $container.append(buildFavoriteThreadRowHtml(nextIndex));
        }
        const $target = $container.find('.favorite-thread-id-input').last();
        if ($target.length) $target.focus();
      }, 0);
    } catch (e) {
      console.warn('[favoriteThreads] open settings failed:', e);
    }
  }
  function ensureSettingsPanelOpen() {
    if (!$('#sp_btn').length) SettingPanel.init();
    if (typeof window.__xdexSyncDarkReaderTheme === 'function') {
      try { window.__xdexSyncDarkReaderTheme(); } catch (e) {}
    }
    const $cover = $('#sp_cover');
    if ($cover.length && !$cover.is(':visible')) $cover.fadeIn();
    if (typeof spUpdate === 'function') {
      try { spUpdate('maybeShowPendingUpdateDialogOnPanelOpen'); } catch (e) {}
    } else if (typeof maybeShowPendingUpdateDialogOnPanelOpen === 'function') {
      try { maybeShowPendingUpdateDialogOnPanelOpen(); } catch (e) {}
    }
  }
  function openSettingsPanelModuleTab(moduleName) {
    ensureSettingsPanelOpen();
    // 委托在 #sp_panel_tab_slot 上,需 bubble 的 trigger('click');init 已不再预渲染历史,故只会构建一次
    const $tab = $('#sp_panel_tab_slot [data-sp-module="' + moduleName + '"]');
    if ($tab.length) $tab.trigger('click');
    return $tab.length > 0;
  }
  function openThreadHistorySettingsPanel() {
    try {
      if (!openSettingsPanelModuleTab('history')) renderThreadHistoryModuleSoon();
    } catch (e) {
      console.warn('[thread-history] open settings failed:', e);
    }
  }
  function openPostHistorySettingsPanel() {
    try {
      if (!openSettingsPanelModuleTab('posts')) renderPostHistoryModuleSoon();
    } catch (e) {
      console.warn('[post-history] open settings failed:', e);
    }
  }
  function openSubscriptionFeedSettingsPanel() {
    try {
      if (!openSettingsPanelModuleTab('feeds') && typeof renderSubscriptionFeedModule === 'function') {
        renderSubscriptionFeedModule();
      }
    } catch (e) {
      console.warn('[subscription-feed] open settings failed:', e);
    }
  }
  function setXDexSidebarExLabel(link, label) {
    link.classList.add('xdex-sidebar-ex-link');
    link.textContent = '';
    link.appendChild(document.createTextNode(label));
    const badge = document.createElement('sub');
    badge.className = 'xdex-sidebar-ex-badge';
    badge.style.cssText = 'color: darkorange; font-weight: bold;';
    badge.setAttribute('aria-hidden', 'true');
    badge.textContent = 'EX';
    link.appendChild(badge);
  }
  function createPostHistoryMenuNode() {
    const li = document.createElement('li');
    li.id = 'xdex-post-history-menu';
    const link = document.createElement('a');
    link.href = '#';
    link.className = 'h-nav-parent-header fr-bold-33d0c43d3b0';
    link.setAttribute('achecked', '1');
    setXDexSidebarExLabel(link, '我的发言');
    link.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      openPostHistorySettingsPanel();
      return false;
    };
    li.appendChild(link);
    return li;
  }
  function createSubscriptionFeedMenuNode() {
    const li = document.createElement('li');
    li.id = 'xdex-subscription-feed-menu';
    const link = document.createElement('a');
    link.href = '#';
    link.className = 'h-nav-parent-header fr-bold-33d0c43d3b0';
    link.setAttribute('achecked', '1');
    setXDexSidebarExLabel(link, '我的订阅');
    link.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      openSubscriptionFeedSettingsPanel();
      return false;
    };
    li.appendChild(link);
    return li;
  }
  function addCurrentThreadToFavoriteThreads() {
    const threadId = getCurrentFavoriteThreadId();
    if (!threadId) {
      openFavoriteThreadsSettingsPanel({ addEmptyGroup: true });
      toast('请在常用串设置中手动填写');
      return;
    }
    const cfg = getFavoriteThreadsConfig();
    const duplicateIndex = cfg.favoriteThreads.findIndex((item) => item.threadId === threadId);
    if (duplicateIndex >= 0) {
      const dup = cfg.favoriteThreads[duplicateIndex];
      const suffix = dup.desc ? `(${dup.desc})` : '';
      toast(`当前串已在第${duplicateIndex + 1}组${suffix}`);
      return;
    }
    const settings = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}), SettingPanel.state || {});
    settings.favoriteThreads = normalizeFavoriteThreads([...(cfg.favoriteThreads || []), {
      desc: getCurrentFavoriteThreadDesc(),
      threadId,
    }]);
    SettingPanel.state = settings;
    GM_setValue(SettingPanel.key, settings);
    try { SettingPanel.syncInputs(); } catch (e) {}
    renderFavoriteThreadsMenu();
    toast('已添加当前串到常用串');
  }
  function ensureFavoriteThreadsMenuStyle() {
    if (document.getElementById('xdex-favorite-threads-menu-style')) return;
    const style = document.createElement('style');
    style.id = 'xdex-favorite-threads-menu-style';
    style.textContent = `
      #h-menu-content .xdex-sidebar-ex-badge {
        color: darkorange;
        font-weight: bold;
        font-size: 10px;
        line-height: 1;
        margin-left: 1px;
      }
      #h-menu-content.uk-nav-parent-icon > #xdex-favorite-threads-menu > a::after {
        content: "\f104";
        width: 20px;
        margin-right: -10px;
        float: right;
        font-family: FontAwesome;
        text-align: center;
      }
      #h-menu-content.uk-nav-parent-icon > #xdex-favorite-threads-menu.uk-open > a::after {
        content: "\f107";
      }
      #xdex-favorite-threads-menu .xdex-favorite-add-current {
        text-decoration: underline;
      }
    `;
    document.head.appendChild(style);
  }
  function createFavoriteThreadsMenuNode(items, isOpen = false) {
    const li = document.createElement('li');
    li.id = 'xdex-favorite-threads-menu';
    li.className = 'uk-parent';
    if (isOpen) li.classList.add('uk-open');
    const header = document.createElement('a');
    header.href = '#';
    header.className = 'h-nav-parent-header fr-bold-33d0c43d3b0';
    header.setAttribute('achecked', '1');
    header.setAttribute('onclick', 'return false;');
    setXDexSidebarExLabel(header, '常用串');
    li.appendChild(header);
    const wrapper = document.createElement('div');
    wrapper.style.cssText = `overflow:hidden;height:${isOpen ? 'auto' : '0'};position:relative;`;
    wrapper.classList.toggle('uk-hidden', !isOpen);
    const list = document.createElement('ul');
    list.className = 'uk-nav-sub';
    const addItem = document.createElement('li');
    const addLink = document.createElement('a');
    addLink.href = '#';
    addLink.className = 'xdex-favorite-add-current';
    addLink.setAttribute('achecked', '1');
    addLink.textContent = getFavoriteThreadsAddLinkText();
    addLink.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      addCurrentThreadToFavoriteThreads();
      return false;
    };
    addItem.appendChild(addLink);
    list.appendChild(addItem);
    items.forEach((item) => {
      const subItem = document.createElement('li');
      const link = document.createElement('a');
      link.href = getLatestThreadHistoryUrl(item.threadId) || makeFavoriteThreadUrl(item.threadId);
      link.dataset.threadId = item.threadId;
      link.target = '_blank';
      link.rel = 'noopener noreferrer';
      link.setAttribute('achecked', '1');
      link.textContent = formatFavoriteThreadMenuText(item);
      link.title = item.desc ? `${item.desc} - ${item.threadId}` : '';
      subItem.appendChild(link);
      list.appendChild(subItem);
    });
    wrapper.appendChild(list);
    li.appendChild(wrapper);
    if (window.jQuery) $(li).data('list-container', $(wrapper));
    li.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
    header.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopImmediatePropagation();
      const opening = !li.classList.contains('uk-open');
      li.classList.toggle('uk-open', opening);
      li.setAttribute('aria-expanded', opening ? 'true' : 'false');
      if (window.jQuery) {
        if (opening) wrapper.classList.remove('uk-hidden');
        $(wrapper).stop(true, true).animate({ height: opening ? $(list).outerHeight() : 0 }, () => {
          if (opening && li.classList.contains('uk-open')) {
            wrapper.style.height = 'auto';
            wrapper.classList.remove('uk-hidden');
          } else if (!opening) {
            wrapper.style.height = '0';
            wrapper.classList.add('uk-hidden');
          }
        });
        return;
      }
      if (opening) {
        wrapper.classList.remove('uk-hidden');
        wrapper.style.height = `${list.scrollHeight}px`;
        window.setTimeout(() => {
          if (li.classList.contains('uk-open')) wrapper.style.height = 'auto';
        }, 160);
      } else {
        wrapper.style.height = `${list.scrollHeight}px`;
        window.requestAnimationFrame(() => {
          wrapper.style.height = '0';
          wrapper.classList.add('uk-hidden');
        });
      }
    });
    return li;
  }
  function createThreadHistoryMenuNode() {
    const li = document.createElement('li');
    li.id = 'xdex-thread-history-menu';
    const link = document.createElement('a');
    link.href = '#';
    link.className = 'h-nav-parent-header fr-bold-33d0c43d3b0';
    link.setAttribute('achecked', '1');
    setXDexSidebarExLabel(link, '浏览历史');
    link.onclick = (e) => {
      e.preventDefault();
      e.stopPropagation();
      openThreadHistorySettingsPanel();
      return false;
    };
    li.appendChild(link);
    return li;
  }
  function renderFavoriteThreadsMenu() {
    const menu = document.getElementById('h-menu-content') || document.querySelector('.h-menu-content');
    if (!menu) return;
    menu.classList.add('uk-nav-parent-icon');
    ensureFavoriteThreadsMenuStyle();
    const old = document.getElementById('xdex-favorite-threads-menu');
    const oldThreadHistory = document.getElementById('xdex-thread-history-menu');
    const oldPostHistory = document.getElementById('xdex-post-history-menu');
    const oldSubscriptionFeed = document.getElementById('xdex-subscription-feed-menu');
    const wasOpen = !!(old && old.classList.contains('uk-open'));
    if (old) old.remove();
    if (oldThreadHistory) oldThreadHistory.remove();
    if (oldPostHistory) oldPostHistory.remove();
    if (oldSubscriptionFeed) oldSubscriptionFeed.remove();
    const items = getFavoriteThreadsConfig().favoriteThreads || [];
    const node = createFavoriteThreadsMenuNode(items, wasOpen);
    // 历史开关关闭时对应入口不出现(已有数据保留,仅隐藏入口)
    const threadHistoryNode = isThreadHistoryEnabledNow() ? createThreadHistoryMenuNode() : null;
    const postHistoryNode = isPostHistoryEnabledNow() ? createPostHistoryMenuNode() : null;
    const subscriptionFeedNode = createSubscriptionFeedMenuNode();
    const timeline = Array.from(menu.children).find((li) => {
      const header = li && li.querySelector ? li.querySelector(':scope > .h-nav-parent-header, :scope > a') : null;
      return header && (header.textContent || '').trim() === '时间线';
    });
    menu.insertBefore(node, timeline || menu.firstChild);
    let insertAfter = node;
    if (threadHistoryNode) { menu.insertBefore(threadHistoryNode, timeline || insertAfter.nextSibling); insertAfter = threadHistoryNode; }
    if (postHistoryNode) { menu.insertBefore(postHistoryNode, timeline || insertAfter.nextSibling); insertAfter = postHistoryNode; }
    menu.insertBefore(subscriptionFeedNode, timeline || insertAfter.nextSibling);
  }
  // 同步常用串菜单与设置面板串内链接的 href(浏览历史更新后调用,指向最近查看页,允许低于最远访问页)
  function syncFavoriteThreadsLinks() {
    const links = document.querySelectorAll('#xdex-favorite-threads-menu a[data-thread-id], #sp_panel_footer a[data-thread-id]');
    if (!links.length) return;
    const store = getThreadHistoryStore();
    links.forEach(link => {
      const tid = link.dataset.threadId;
      if (tid) {
        // 优先普通模式,没有再回退只看 PO;都没有则指向普通第 1 页
        const normal = store.items[getThreadHistoryKey('normal', tid)];
        const item = normal || store.items[getThreadHistoryKey('po', tid)];
        if (item) {
          const page = item.page || 1;
          link.href = buildThreadHistoryPageUrl(item.mode, tid, page);
        } else {
          link.href = buildThreadHistoryPageUrl('normal', tid, 1);
        }
      }
    });
  }
  // ─── 页面内「订阅EX」快捷按钮 ─────────────────────────────────────────────────────────
  function ensureSubscriptionExButtonStyle() {
    if (document.getElementById('xdex-subscription-ex-btn-style')) return;
    const style = document.createElement('style');
    style.id = 'xdex-subscription-ex-btn-style';
    style.textContent = `
      .xdex-sub-ex-btn { color: #00FFCC; font-weight: normal; }
      .xdex-sub-ex-btn .xdex-sidebar-ex-badge {
        color: #66CCFF;
        font-weight: bold;
        font-size: 10px;
        line-height: 1;
        margin-left: 1px;
      }
      .xdex-sub-ex-btn:hover { text-decoration: underline; }
      /* 深色模式:信息行链接提亮规则(!important)会盖掉品牌色,这里以同特异性+后源顺序夺回 */
      :root.xdex-custom-dark .h-threads-item .h-threads-info a.xdex-sub-ex-btn { color: #00FFCC !important; }
      :root.xdex-custom-dark .h-threads-item .h-threads-info a.xdex-sub-ex-btn .xdex-sidebar-ex-badge { color: #66CCFF !important; }
      .xdex-feed-selector-display {
        display: flex; align-items: center; gap: 6px;
        padding: 6px 8px;
        border: 1px solid var(--xdex-sp-border, #bfa58f);
        border-radius: 8px;
        background: var(--xdex-sp-panel-bg, #FFFFEE);
        cursor: pointer;
        font-size: 13px;
        line-height: 1.4;
        min-height: 1.4em;
        user-select: none;
        overflow: hidden;
      }
      .xdex-feed-display-desc {
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
      }
      .xdex-feed-display-uuid {
        font-size: 12px; color: #888;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        filter: blur(4px);
        transition: filter 0.15s;
      }
      /* 无备注时仅显示订阅号:模糊稍浅,能感知有内容被打码,但看不清具体字符 */
      .xdex-feed-display-uuid.xdex-feed-uuid-light-blur {
        filter: blur(2.5px);
        opacity: 0.85;
      }
      .xdex-feed-display-uuid:empty { display: none; }
      .xdex-feed-chevron {
        flex: 0 0 auto; margin-left: auto;
        color: #C61200; transition: transform .15s ease;
        transform: rotate(0deg);
      }
      .xdex-feed-selector-display[aria-expanded="true"] .xdex-feed-chevron {
        transform: rotate(-90deg);
      }
      .xdex-feed-selector-dropdown {
        position: absolute; top: 100%; left: 0; right: 0;
        margin-top: 2px;
        border: 1px solid var(--xdex-sp-border, #bfa58f);
        border-radius: 8px;
        background: var(--xdex-sp-panel-bg, #FFFFEE);
        box-shadow: 0 2px 8px var(--xdex-sp-shadow, rgba(0,0,0,.15));
        z-index: 100;
        max-height: 200px; overflow-y: auto;
      }
      .xdex-feed-option {
        padding: 6px 8px;
        cursor: pointer;
        font-size: 13px;
        /* --foreground 未定义会恒取 #333,深色下与 #2b2c2d 底同暗不可见;改为继承面板前景(与选择框本体一致) */
        color: inherit;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
      }
      .xdex-feed-option:hover { background: #F0E0D6; }
      .xdex-feed-option.active { background: #F0E0D6; font-weight: bold; }
      :root.xdex-darkreader-active .xdex-feed-option:hover { background: #3d3530; }
      :root.xdex-darkreader-active .xdex-feed-option.active { background: #483327; }
    `;
    document.head.appendChild(style);
  }
  function resolveSubscriptionFeedUuid() {
    const panelSelectedUuid = String($('#sp_feeds_selector').val() || '').trim();
    if (panelSelectedUuid) return panelSelectedUuid;
    const dropdownSelectedUuid = String($('#sp_feeds_selector_dropdown .xdex-feed-option.active').data('uuid') || '').trim();
    if (dropdownSelectedUuid) return dropdownSelectedUuid;
    const uuid = subscriptionFeedCurrentUuid || getActiveSubscriptionFeedUuid() || '';
    if (uuid) {
      const feeds = (typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [];
      if (feeds.some((f) => String(f.uuid || '').trim() === uuid)) return uuid;
    }
    const feeds = (typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [];
    return feeds.length ? feeds[0].uuid : '';
  }
  // 订阅到指定订阅号(默认值直订 / 弹窗选择共用)
  function subscribeThreadToFeed(tid, uuid) {
    const targetTid = String(tid || '').trim();
    const targetUuid = String(uuid || '').trim();
    if (!targetTid || !targetUuid) return;
    const feeds = (typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [];
    const feed = feeds.find((f) => String((f && f.uuid) || '').trim() === targetUuid);
    const label = (feed && feed.desc) ? feed.desc : ((feed && feed.uuid) || targetUuid);
    GM_xmlhttpRequest({
      method: 'POST',
      url: `${SUBSCRIPTION_FEED_API_BASE}/addFeed`,
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      data: `uuid=${encodeURIComponent(targetUuid)}&tid=${encodeURIComponent(targetTid)}`,
      onload: (resp) => {
        if (resp.status >= 200 && resp.status < 300) {
          invalidateSubscriptionFeedSession(targetUuid, 'add-feed');
          toast(`已订阅 No.${targetTid} 到「${label}」`);
        } else {
          toast(`订阅失败 (${resp.status})`);
        }
      },
      onerror: () => toast('订阅失败,网络错误')
    });
  }

  // 选择订阅号弹窗:未设置默认订阅号时,点击“订阅EX”每次弹出。
  // 结构对齐 showCookieConfirmDialog:标题 + 串号(接入引用格式)+ 列表(每行尾部带“默认”开关与默认金标)
  // + 键盘上下选择/Enter 确认 + 下限高度、列表滚动;无用量统计等订阅场景不需要的元素。
  function showSubscriptionFeedPicker(tid) {
    const targetTid = String(tid || '').trim();
    if (!targetTid) return;
    if (document.getElementById('subscription-feed-picker-modal')) return;
    const feeds = ((typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [])
      .filter((f) => f && String(f.uuid || '').trim());
    if (!feeds.length) { toast('请先在设置中添加一个订阅号'); return; }
    let selectedUuid = String(feeds[0].uuid || '').trim();
    let focusedIndex = 0;
    const $m = $(`
      <div id="subscription-feed-picker-modal" tabindex="-1" style="position:fixed;inset:0;z-index:10002;display:flex;align-items:center;justify-content:center;outline:none;">
        <div class="subscription-feed-picker-backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,.4);"></div>
        <div class="subscription-feed-picker-dialog" role="dialog" aria-modal="true" aria-labelledby="subscription-feed-picker-title" style="position:relative;display:flex;flex-direction:column;width:420px;max-width:calc(100vw - 32px);min-height:min(280px, calc(100vh - 32px));max-height:min(80vh, calc(100vh - 32px));box-sizing:border-box;background:var(--xdex-sp-panel-bg, #FFFFEE);border:1px solid var(--xdex-sp-border, #ccc);border-radius:8px;box-shadow:0 2px 12px var(--xdex-sp-shadow, rgba(0,0,0,.25));padding:16px;overflow:hidden;">
          <h3 id="subscription-feed-picker-title" style="margin:0 0 8px;font-size:16px;color:var(--foreground, #333);flex:0 0 auto;">选择订阅号</h3>
          <p style="margin:0 0 10px;display:flex;justify-content:space-between;align-items:center;gap:8px;color:var(--muted-foreground, #666);font-size:13px;flex:0 0 auto;">
            <span>订阅 <span id="subscription-feed-picker-tid"></span> 到:</span>
            <span id="subscription-feed-picker-default-label" style="color:#ffb300;font-size:11px;white-space:nowrap;"></span>
          </p>
          <div id="subscription-feed-picker-list" style="flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding-right:2px;"></div>
          <div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--xdex-sp-border, #ddd);font-size:12px;line-height:1.45;color:var(--muted-foreground, #888);flex:0 0 auto;">设为默认后,之后点击“订阅EX”将直接订阅到默认订阅号,不再弹窗二次确认;如需按分类分别订阅,建议保持关闭。</div>
          <div id="subscription-feed-picker-actions" style="display:flex;align-items:center;gap:4px;margin-top:10px;flex:0 0 auto;">
            <button id="subscription-feed-picker-ok" type="button" style="flex:1;padding:6px 10px;">订阅</button>
            <button id="subscription-feed-picker-cancel" type="button" style="padding:6px 16px;flex-shrink:0;">取消</button>
          </div>
        </div>
      </div>`);
    const $list = $m.find('#subscription-feed-picker-list');
    const feedLabel = (feed) => (feed && feed.desc) ? feed.desc : ((feed && feed.uuid) || '');
    // 主按钮文案:选中项 + 是否为默认(与饼干二次确认一致)
    const updateOk = () => {
      const feed = feeds.find((f) => String(f.uuid || '').trim() === selectedUuid);
      const isDefault = getDefaultSubscriptionFeedUuid() === selectedUuid;
      $m.find('#subscription-feed-picker-ok').text(`订阅到「${feedLabel(feed) || selectedUuid}」` + (isDefault ? '(默认)' : ''));
    };
    // 右上金色「默认: xxx」文本 + 各行的默认金标/开关选中态
    const refreshDefaultVisuals = () => {
      const cur = getDefaultSubscriptionFeedUuid();
      const $lab = $m.find('#subscription-feed-picker-default-label');
      const curFeed = feeds.find((f) => String(f.uuid || '').trim() === cur);
      if (curFeed) $lab.text('默认: ' + feedLabel(curFeed)); else $lab.text('');
      $list.find('.subscription-feed-picker-item').each(function () {
        const uuid = String($(this).attr('data-uuid') || '');
        const isDef = uuid === cur;
        $(this).find('.subscription-feed-picker-default').prop('checked', isDef);
        $(this).find('.xdex-feed-picker-default-badge').toggle(!!isDef);
      });
      updateOk();
    };
    feeds.forEach((feed) => {
      const uuid = String(feed.uuid || '').trim();
      const desc = String(feed.desc || '').trim();
      // 复用面板下拉的 .xdex-feed-option(hover 底色),用内联样式覆盖为行布局
      const $item = $('<div class="xdex-feed-option subscription-feed-picker-item" role="option"></div>')
        .attr('data-uuid', uuid)
        .css({
          display: 'flex', alignItems: 'center', gap: '8px',
          padding: '8px 10px',
          border: '1px solid var(--xdex-sp-border, #ddd)',
          borderRadius: '6px',
          marginBottom: '6px',
          cursor: 'pointer',
          whiteSpace: 'normal', overflow: 'visible', textOverflow: 'clip'
        });
      const $text = $('<div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;"></div>');
      // 备注优先;无备注时直接显示订阅号
      const $main = $('<div style="font-size:13px;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></div>');
      $main.append($('<span></span>').text(desc || uuid));
      $main.append($('<span class="xdex-feed-picker-default-badge" style="display:none;margin-left:4px;background:rgba(255,193,7,.18);color:#ffb300;padding:1px 5px;border-radius:3px;font-size:11px;">默认</span>'));
      $text.append($main);
      if (desc) {
        $text.append($('<div style="font-size:12px;color:var(--muted-foreground, #888);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"></div>').text(uuid));
      }
      $item.append($text);
      // 「默认」开关置于订阅号后方(与饼干二次确认的 pin 开关同位)
      const $sw = $('<input type="checkbox" class="xdex-switch subscription-feed-picker-default" role="switch">')
        .attr('title', '设为本机默认订阅号:之后点击“订阅EX”将直接订阅到该订阅号,不再询问');
      const $swLabel = $('<label style="font-size:12px;color:#6f5d50;user-select:none;">默认</label>');
      $item.append($('<div style="display:flex;align-items:center;gap:6px;flex:0 0 auto;white-space:nowrap;"></div>').append($sw).append($swLabel));
      $item.on('click', (e) => {
        // 点在开关上不触发选中
        if ($(e.target).closest('.subscription-feed-picker-default').length) return;
        selectItem($item);
      });
      // 立即写入默认(与设置面板行内开关、跨页同步同源),无需等确认
      $sw.on('click', (e) => e.stopPropagation());
      $sw.on('change', () => {
        const on = $sw.is(':checked');
        if (on) {
          $list.find('.subscription-feed-picker-default').prop('checked', false);
          $sw.prop('checked', true);
          setDefaultSubscriptionFeedUuid(uuid);
        } else if (getDefaultSubscriptionFeedUuid() === uuid) {
          setDefaultSubscriptionFeedUuid('');
        }
        refreshDefaultVisuals();
      });
      $list.append($item);
    });
    const $items = () => $list.find('.subscription-feed-picker-item');
    function selectItem($item) {
      const idx = $items().toArray().indexOf($item[0]);
      if (idx < 0) return;
      focusedIndex = idx;
      selectedUuid = String(feeds[idx].uuid || '').trim();
      $items().css({ background: '', borderColor: 'var(--xdex-sp-border, #ddd)' });
      $item.css({ background: 'rgba(0,184,148,.12)', borderColor: '#00b894' });
      updateOk();
    }
    // 键盘上下移动焦点(同步选中态与按钮文案),Enter 确认
    const updateFocus = (newIndex) => {
      const $all = $items();
      if (!$all.length) return;
      focusedIndex = Math.max(0, Math.min(newIndex, $all.length - 1));
      $all.css('box-shadow', '');
      $all.eq(focusedIndex).css('box-shadow', 'inset 0 0 0 2px rgba(102,204,255,.6)').get(0)?.scrollIntoView({ block: 'nearest' });
      selectItem($all.eq(focusedIndex));
    };
    $('body').append($m);
    // 串号接入引用体系:悬停原生引用浮窗 / 点击拓展引用浮窗(与饼干二次确认同路径)
    $m.find('#subscription-feed-picker-tid').html('<font color="#789922">No.' + targetTid + '</font>');
    try { if (typeof initExtendedContent === 'function') initExtendedContent($m[0]); } catch (e) {}
    // 初始:选中第一项、刷新默认金标/开关/文案
    $items().first().trigger('click');
    refreshDefaultVisuals();
    const closePicker = () => { $m.remove(); };
    $m.find('#subscription-feed-picker-ok').on('click', () => {
      closePicker();
      subscribeThreadToFeed(targetTid, selectedUuid);
    });
    $m.find('#subscription-feed-picker-cancel').on('click', closePicker);
    $m.find('.subscription-feed-picker-backdrop').on('click', closePicker);
    $m.on('keydown', (e) => {
      // 焦点在按钮上时交给原生行为(Enter 触发该按钮),避免把「取消」误当成确认
      if (e.key === 'Enter' && $(e.target).is('button')) return;
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        e.stopPropagation();
        updateFocus(focusedIndex + 1);
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        e.stopPropagation();
        updateFocus(focusedIndex - 1);
      } else if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey && !e.altKey) {
        e.preventDefault();
        e.stopPropagation();
        $m.find('#subscription-feed-picker-ok').trigger('click');
      } else if (e.key === 'Escape') {
        e.preventDefault();
        e.stopPropagation();
        closePicker();
      }
    });
    $m[0].focus();
  }

  function injectSubscriptionExButton(root) {
    ensureSubscriptionExButtonStyle();
    const scope = root || document;
    const containers = scope.querySelectorAll ? scope.querySelectorAll('.h-threads-item[data-threads-id] > .h-threads-item-main') : [];
    containers.forEach((container) => {
      // 找到 No.xxx 链接作为插入锚点(板块页和串内页都有)
      const anchor = container.querySelector('a.h-threads-info-id');
      if (!anchor || anchor.__xdexSubExBound) return;
      anchor.__xdexSubExBound = true;
      const sep = document.createTextNode(' ');
      const span = document.createElement('span');
      span.className = 'h-threads-info-report-btn';
      const a = document.createElement('a');
      a.href = '#';
      a.title = '订阅到订阅号(未设默认时弹窗选择)';
      a.className = 'xdex-sub-ex-btn';
      a.appendChild(document.createTextNode('订阅'));
      const badge = document.createElement('sub');
      badge.className = 'xdex-sidebar-ex-badge';
      badge.style.cssText = 'color: #66CCFF; font-weight: bold;';
      badge.setAttribute('aria-hidden', 'true');
      badge.textContent = 'EX';
      a.appendChild(badge);
      a.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const tid = String(container.getAttribute('data-threads-id') || anchor.closest('.h-threads-item')?.getAttribute('data-threads-id') || '').trim();
        if (!tid) return;
        const feeds = (typeof getFilterConfig === 'function' ? getFilterConfig() : {}).subscriptionFeeds || [];
        if (!feeds.length) {
          toast('请先在设置中添加一个订阅号');
          return;
        }
        // 存在有效默认订阅号 → 直接订阅(不再弹窗确认);否则每次点击都弹窗选择订阅号
        const defaultUuid = getDefaultSubscriptionFeedUuid();
        if (defaultUuid) {
          subscribeThreadToFeed(tid, defaultUuid);
          return;
        }
        showSubscriptionFeedPicker(tid);
      });
      span.appendChild(document.createTextNode('['));
      span.appendChild(a);
      span.appendChild(document.createTextNode(']'));
      anchor.before(span, sep);
    });
  }

  /* --------------------------------------------------
   * tag 25. 发送前饼干二次确认
   * -------------------------------------------------- */
  function getThreadCookieStats(threadId) {
    if (!threadId) return {};
    const store = getPostHistoryStore();
    const stats = {};
    let lastUsedHash = '';
    let lastUsedTs = 0;
    store.order.forEach(key => {
      const item = store.items[key];
      if (!item) return;
      const hash = String(item.userHash || '').trim();
      if (!hash) return;
      const itemThreadId = String(item.threadId || (item.type === 'reply' ? item.resto : item.id) || '');
      if (itemThreadId !== threadId) return;
      if (!stats[hash]) stats[hash] = { count: 0, lastUsed: 0 };
      stats[hash].count++;
      const ts = Number(item.confirmedAt || item.submittedAt) || 0;
      if (ts > stats[hash].lastUsed) stats[hash].lastUsed = ts;
      if (ts > lastUsedTs) { lastUsedTs = ts; lastUsedHash = hash; }
    });
    const cookieList = getCookiesList();
    Object.keys(stats).forEach(hash => {
      const cookie = cookieList[hash];
      stats[hash].name = cookie ? abbreviateName(cookie.name || '').replace(/ - 0000-00-00 00:00:00$/g, '').trim() : hash;
      stats[hash].desc = cookie ? (cookie.desc || '') : '';
      stats[hash].isLastUsed = (hash === lastUsedHash && lastUsedTs > 0);
    });
    return stats;
  }
  function showCookieConfirmDialog(threadId, onConfirm, onCancel, opts) {

    opts = opts || {};
    const mode = opts.mode || 'send';
    const preselectHash = opts.preselectHash || '';
    const stats = getThreadCookieStats(threadId);
    const cookieList = getCookiesList();
    const currentCookie = getCurrentCookie();
    const currentId = currentCookie ? String(currentCookie.id) : '';
    // 与饼干下拉列表排序一致(Object.keys 顺序)
    const allCookies = Object.keys(cookieList).map(id => cookieList[id]);
    const pref = getThreadCookiePref(threadId);
    let selectedId = currentId;
    if (preselectHash) {
      const match = allCookies.find(c => abbreviateName(c.name) === preselectHash);
      if (match) selectedId = String(match.id);
    }
    let pinnedHash = null;
    const $m = $(
        '<div id="cookie-confirm-modal" tabindex="-1" style="position:fixed;inset:0;z-index:10002;display:flex;align-items:center;justify-content:center;outline:none;">' +
        '<div class="cookie-confirm-backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,.4);"></div>' +
        '<div class="cookie-confirm-dialog" style="position:relative;display:flex;flex-direction:column;width:360px;max-width:calc(100vw - 32px);max-height:min(80vh, calc(100vh - 32px));background:var(--xdex-sp-panel-bg, #FFFFEE);border:1px solid var(--xdex-sp-border, #ccc);border-radius:8px;box-shadow:0 2px 12px var(--xdex-sp-shadow, rgba(0,0,0,.25));padding:16px;overflow:hidden;">' +
          '<h3 style="margin:0 0 8px;font-size:16px;color:var(--foreground, #333);flex:0 0 auto;">' + (mode === 'setDefault' ? '设置默认饼干' : '确认饼干') + '</h3>' +
          '<p style="margin:0 0 10px;display:flex;justify-content:space-between;align-items:center;color:var(--muted-foreground, #666);font-size:13px;flex:0 0 auto;"><span>当前串 <font color="#789922">No.' + threadId + '</font></span>' + (pref ? '<span style="color:#ffb300;font-size:11px;">默认: ' + (abbreviateName((findCookieByHash(cookieList, pref.hash) || {}).name || pref.hash || '').replace(/ - 0000-00-00 00:00:00$/g, '').trim()) + '</span>' : '') + '</p>' +
          '<div id="cookie-confirm-scroll" style="flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding-right:2px;">' +
            '<div id="cookie-confirm-list"></div>' +
          '</div>' +
          '<div id="cookie-confirm-tail-spacer" style="flex:0 0 auto;height:48px;flex-shrink:0;"></div>' +
          '<div id="cookie-confirm-await-marker" aria-hidden="true" style="flex:0 0 0;border-top:1px solid var(--xdex-sp-border, #ddd);margin:0;flex-shrink:0;"></div>' +
          '<div id="cookie-confirm-actions" style="display:flex;align-items:center;gap:4px;flex:0 0 auto;flex-shrink:0;">' +
            '<button id="cookie-confirm-ok" style="flex:1;padding:6px 10px;">使用当前饼干发送</button>' +
            '<button id="cookie-confirm-cancel" style="padding:6px 16px;flex-shrink:0;">取消</button>' +
          '</div>' +
        '</div>' +
      '</div>'
    );
    function updateOkButton() {
      const sel = allCookies.find(c => String(c.id) === selectedId);
      const selHash = sel ? abbreviateName(sel.name) : '';
      const selName = sel ? abbreviateName(sel.name || '').replace(/ - 0000-00-00 00:00:00$/g, '').trim() : '';
      const prefNow = getThreadCookiePref(threadId);
      const defaultHash = prefNow ? prefNow.hash : '';
      const isSelDefault = defaultHash && selHash === defaultHash;
      if (mode === 'setDefault') {
        $m.find('#cookie-confirm-ok').text('将' + selName + '设为默认');
      } else if (selectedId === currentId) {
        $m.find('#cookie-confirm-ok').text('使用当前饼干发送' + (isSelDefault ? '(默认)' : ''));
      } else {
        $m.find('#cookie-confirm-ok').text('切换到 ' + selName + ' 并发送' + (isSelDefault ? '(默认)' : ''));
      }
    }
    const $list = $m.find('#cookie-confirm-list');
    allCookies.forEach(cookie => {
      const id = String(cookie.id);
      const cookieHash = abbreviateName(cookie.name || '');
      const st = stats[cookieHash];
      const count = st ? st.count : 0;
      const isCurrent = id === currentId;
      const isLastUsed = st && st.isLastUsed;
      const rawName = abbreviateName(cookie.name || '').replace(/ - 0000-00-00 00:00:00$/g, '').trim();
      const rawDesc = (cookie.desc || '').trim();
      const hasDesc = rawDesc && !/^0{4}-0{2}-0{2}/.test(rawDesc);
      const displayName = rawName + (hasDesc ? ' - ' + rawDesc : '');
      const isDefault = (pref && pref.hash === cookieHash);
      let statsHtml = '';
      if (isDefault) {
        statsHtml = '<span style="background:rgba(255,193,7,.18);color:#ffb300;padding:1px 5px;border-radius:3px;font-size:11px;margin-right:4px;">默认</span>';
      }
      if (isLastUsed && count > 0) {
        statsHtml += '<span style="background:rgba(168,46,96,.18);color:#A82E60;padding:1px 5px;border-radius:3px;font-size:11px;">上次' + count + '</span>';
      } else if (count > 0) {
        statsHtml += '<span style="background:rgba(102,204,255,.15);color:#66CCFF;padding:1px 5px;border-radius:3px;font-size:11px;">用过' + count + '</span>';
      } else if (!isDefault) {
        statsHtml += '<span style="color:var(--muted-foreground, #888);font-size:12px;">无记录</span>';
      }
      const selectedBg = isCurrent ? 'background:rgba(0,184,148,.12);border-color:#00b894;' : '';
      const $item = $(
        '<div style="display:flex;align-items:center;gap:4px;">' +
          '<div data-cookie-id="' + id + '" data-cookie-hash="' + cookieHash + '" style="flex:1;display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border:1px solid var(--xdex-sp-border, #ddd);border-radius:6px;cursor:pointer;color:var(--foreground, #333);' + selectedBg + '">' +
            '<span class="h-threads-info-uid" data-xdex-cookie-id="' + cookieHash + '">' + displayName + '</span>' +
            '<span>' + statsHtml + '</span>' +
          '</div>' +
          '<input type="checkbox" class="xdex-switch xdex-pin-btn" data-pin-hash="' + cookieHash + '" role="switch" title="设为本串默认"' + (isDefault ? ' checked' : '') + '>' +
        '</div>'

      );
      $item.on('click', (e) => {
        if ($(e.target).hasClass('xdex-pin-btn')) return;
        $list.find('[data-cookie-id]').css({ background: '', borderColor: 'var(--xdex-sp-border, #ddd)' });

        $item.find('[data-cookie-id]').css({ background: 'rgba(0,184,148,.12)', borderColor: '#00b894' });
        selectedId = id;
        // 双重校验:DOM 引用 + data-cookie-id 属性,任一命中即可(防 eq(-1) 选到最后一个)
        const $allClick = $list.find('[data-cookie-id]');
        const innerEl = $item.find('[data-cookie-id]')[0];
        focusedIndex = $allClick.toArray().findIndex(el => el === innerEl);
        if (focusedIndex < 0) {
          focusedIndex = $allClick.toArray().findIndex(el => $(el).attr('data-cookie-id') === String(id));
        }
        updateOkButton();
      });
      // switch 点击设为默认饼干(不发送、不关闭弹窗)
      $item.find('.xdex-pin-btn').on('click', (e) => {
        e.stopPropagation();
        // e.target.checked 是点击后的状态,取反得到点击前的状态
        const isChecked = e.target.checked;

        if (isChecked) {
          // 点击后是打开状态 → 刚刚从关闭切换到打开:设置偏好(互斥)
          $list.find('.xdex-pin-btn').prop('checked', false);
          $item.find('.xdex-pin-btn').prop('checked', true);
          setThreadCookiePref(threadId, cookieHash);
          // 更新右上角默认提示
          const $existing = $m.find('p span[style*="color:#ffb300"], p span[style*="color: rgb(255, 179, 0)"]');
          if ($existing.length) {
            $existing.text('默认: ' + rawName);
          } else {
            $m.find('p').append('<span style="color:#ffb300;font-size:11px;">默认: ' + rawName + '</span>');
          }
          toast('已将 ' + rawName + ' 设为本串默认饼干');
        } else {
          // 点击后是关闭状态 → 刚刚从打开切换到关闭:清除偏好
          $item.find('.xdex-pin-btn').prop('checked', false);
          removeThreadCookiePref(threadId);
          // 移除右上角默认提示
          $m.find('p span[style*="color:#ffb300"], p span[style*="color: rgb(255, 179, 0)"]').remove();
          // 同步关闭外部的饼干偏好开关
          const outerSwitch = document.querySelector('.xdex-cookie-check-sw');
          if (outerSwitch) {
            outerSwitch.checked = false;
            outerSwitch.dispatchEvent(new Event('change'));
          }
          toast('已清除当前串的偏好饼干');
        }
        // 手动设置或清除默认饼干后,同步外部开关状态与圆圈着色
        if (typeof refreshCookiePrefSwitchState === 'function') refreshCookiePrefSwitchState();
        updateOkButton();
      });
      $list.append($item);
    });
    // preselectHash 选中视觉
    if (preselectHash) {
      const $pre = $list.find('[data-cookie-hash="' + preselectHash + '"]');
      if ($pre.length) {
        $list.find('[data-cookie-id]').css({ background: '', borderColor: 'var(--xdex-sp-border, #ddd)' });
        $pre.css({ background: 'rgba(0,184,148,.12)', borderColor: '#00b894' });
      }
    }
    updateOkButton();
    $m.find('#cookie-confirm-ok').on('click', () => {
      const targetCookie = cookieList[selectedId];
      if (!targetCookie) { toast('饼干信息无效'); return; }
      const selectedHash = abbreviateName(targetCookie.name);
      if (mode === 'setDefault') {
        $m.remove();
        onConfirm(selectedHash, selectedHash);
        return;
      }
      if (selectedId === currentId) {
        $m.remove();
        onConfirm(selectedHash, pinnedHash);
      } else {
        switch_cookie(targetCookie, {
          silent: false,
          onDone: () => { $m.remove(); onConfirm(selectedHash, pinnedHash); },
          onFail: () => { toast('切换失败,未发送'); $m.remove(); onCancel(); }
        });
      }
    });
    $m.find('#cookie-confirm-cancel').on('click', () => { $m.remove(); onCancel(); document.querySelector('textarea.h-post-form-textarea')?.focus(); });
    $m.find('.cookie-confirm-backdrop').on('click', () => { $m.remove(); onCancel(); document.querySelector('textarea.h-post-form-textarea')?.focus(); });
    $('body').append($m);
    $m[0].focus();
    // 应用引用格式拓展和标记饼干
    try { if (typeof initExtendedContent === "function") initExtendedContent($m[0]); } catch (e) {}
    try { if (typeof markAllCookies === "function") markAllCookies(getFilterConfig().markedGroups || [], $m[0]); } catch (e) {}
    // ── 键盘导航 ──
    const $items = () => $list.find('[data-cookie-id]');
    let focusedIndex = allCookies.findIndex(c => String(c.id) === currentId);
    if (focusedIndex < 0) focusedIndex = 0;
    function updateFocus(newIndex) {
      const $all = $items();
      if (!$all.length) return;
      focusedIndex = Math.max(0, Math.min(newIndex, $all.length - 1));
      $all.css('box-shadow', '');
      $all.eq(focusedIndex).css('box-shadow', 'inset 0 0 0 2px rgba(102,204,255,.6)').get(0)?.scrollIntoView({ block: 'nearest' });
      // 同步选中状态和按钮文字
      const focusId = $all.eq(focusedIndex).attr('data-cookie-id');
      if (focusId) {
        selectedId = focusId;
        $list.find('[data-cookie-id]').css({ background: '', borderColor: 'var(--xdex-sp-border, #ddd)' });
        $all.eq(focusedIndex).css({ background: 'rgba(0,184,148,.12)', borderColor: '#00b894' });
        updateOkButton();
      }
    }
    updateFocus(focusedIndex);
    // 列表本身可滚动:把聚焦项滚到视区里,避免键盘首次可见性糟糕
    requestAnimationFrame(() => {
      const $all = $items();
      const $focus = $all.eq(focusedIndex);
      if ($focus.length) $focus[0].scrollIntoView({ block: 'center' });
    });
    // ── 测量最后一项真实高度,同步给底部 spacer ──
    const syncTailSpacer = () => {
      const $last = $list.children().last();
      if ($last.length) {
        $m.find('#cookie-confirm-tail-spacer').height($last[0].getBoundingClientRect().height);
      }
    };
    // ── 视口变化 / 旋转时重新约束弹窗高度,让聚焦项可见 ──
    const recomputeViewport = () => {
      const vh = window.innerHeight || document.documentElement.clientHeight;
      const target = Math.max(180, Math.min(vh - 32, Math.floor(vh * 0.8)));
      $m.find('.cookie-confirm-dialog').css('max-height', target + 'px');
      syncTailSpacer();
    };
    requestAnimationFrame(() => { recomputeViewport(); syncTailSpacer(); });
    $(window).on('resize.xdexCookieConfirm orientationchange.xdexCookieConfirm', recomputeViewport);
    $m.on('remove', () => $(window).off('.xdexCookieConfirm'));
    $m.on('keydown', (e) => {
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        e.stopPropagation();
        updateFocus(focusedIndex + 1);
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        e.stopPropagation();
        updateFocus(focusedIndex - 1);
      } else if (e.ctrlKey && e.key === 'Enter') {
        e.preventDefault();
        e.stopPropagation();
        // 直接用当前聚焦的饼干发送
        const $all = $items();
        if ($all.eq(focusedIndex).length) $all.eq(focusedIndex).trigger('click');
        $m.find('#cookie-confirm-ok').trigger('click');
      } else if (e.key === 'Escape') {
        e.preventDefault();
        e.stopPropagation();
        $m.remove();
        onCancel();
        document.querySelector('textarea.h-post-form-textarea')?.focus();
      }
    });
  }
  // ── 串内饼干偏好存储 ──
    const THREAD_COOKIE_PREFS_KEY = 'xdex_thread_cookie_prefs';
    // 一次性清理旧的脏数据(调试期间写入的错误偏好)
    if (!GM_getValue('xdex_cookie_pref_migrated_v1', false)) {
      try { GM_setValue(THREAD_COOKIE_PREFS_KEY, {}); GM_setValue('xdex_cookie_pref_migrated_v1', true); } catch (e) {}
    }
  function getThreadCookiePrefs() {
    try { return GM_getValue(THREAD_COOKIE_PREFS_KEY, {}); } catch (e) { return {}; }
  }
  function getThreadCookiePref(threadId) {
    if (!threadId) return null;
    const pref = getThreadCookiePrefs()[threadId];
    // 如果是清除标记,返回 null
    if (pref && pref.cleared) return null;
    return pref || null;
  }
  function setThreadCookiePref(threadId, hash) {
    if (!threadId || !hash) return;
    const prefs = getThreadCookiePrefs();
    prefs[threadId] = { hash: String(hash), setAt: Date.now() };
    try { GM_setValue(THREAD_COOKIE_PREFS_KEY, prefs); } catch (e) {}
  }
  function removeThreadCookiePref(threadId) {
    if (!threadId) return;
    const prefs = getThreadCookiePrefs();
    // 存储清除标记,防止 initThreadCookiePref 自动重新设置
    prefs[threadId] = { hash: '', cleared: true, clearedAt: Date.now() };
    try { GM_setValue(THREAD_COOKIE_PREFS_KEY, prefs); } catch (e) {}
  }
  // 在 cookieList 中通过 hash 查找饼干(兼容 key 为 id 或 hash 的情况)
  function findCookieByHash(cookieList, hash) {
    if (!cookieList || !hash) return null;
    return Object.values(cookieList).find(c => abbreviateName(c.name || '') === hash) || null;
  }
  // 当前饼干是否命中本串偏好:未开启偏好返回 null,命中 true,未命中 false
  function isCookieMatchingThreadPref(threadId) {
    const pref = getThreadCookiePref(threadId);
    if (!pref || !pref.hash) return null;
    const cur = getCurrentCookie();
    if (!cur) return false;
    return abbreviateName(cur.name || '') === String(pref.hash);
  }
  // 依据判定结果给开关圆圈加/去着色类并同步 tooltip;仅作视觉反馈,不改变任何发送逻辑
  function refreshCookiePrefSwitchState() {
    document.querySelectorAll('.xdex-cookie-check-sw').forEach(sw => {
      const tid = sw.dataset ? (sw.dataset.threadId || '') : '';
      const pref = tid ? getThreadCookiePref(tid) : null;
      const enabled = !!(pref && pref.hash);
      // 开关状态与齿轮可见性同步自实际偏好:未开启偏好(开关关闭)时圆圈恒为白色、无提示
      if (sw.checked !== enabled) sw.checked = enabled;
      const area = sw.closest ? sw.closest('.xdex-cookie-check-area') : null;
      const editBtn = area ? area.querySelector('.xdex-edit-default-btn') : null;
      if (editBtn) {
        editBtn.style.visibility = enabled ? 'visible' : 'hidden';
        editBtn.style.pointerEvents = enabled ? 'auto' : 'none';
      }
      const matched = enabled ? isCookieMatchingThreadPref(tid) : null;
      sw.classList.toggle('xdex-cookie-pref-match', matched === true);
      sw.classList.toggle('xdex-cookie-pref-mismatch', matched === false);
      if (matched === true) {
        sw.title = '当前饼干即为默认饼干';
      } else if (matched === false) {
        const prefHash = String((pref || {}).hash || '');
        const prefCookie = findCookieByHash(getCookiesList(), prefHash);
        const prefName = (prefCookie ? abbreviateName(prefCookie.name || '') : prefHash) || '未设置';
        sw.title = '当前饼干非默认饼干,默认饼干为' + prefName;
      } else {
        sw.removeAttribute('title');
      }
    });
  }
  // ── PO 主饼干检测 ──
  function detectPOCookieHash() {
    // 直接从 PO 的 uid 文本提取 hash(如 "ID:z19vISg" → "z19vISg")
    const uidSpan = document.querySelector('.h-threads-item-main .h-threads-info .h-threads-info-uid');
    if (!uidSpan) return null;
    const rawText = (uidSpan.textContent || '').trim();
    const poHash = rawText.replace(/^ID:\s*/, '').trim();
    if (!poHash) return null;
    return findCookieByHash(getCookiesList(), poHash) ? poHash : null;
  }
  // ── 发言历史检测 ──
  function detectHistoryCookieHash(threadId) {
    if (!threadId) return null;
    const store = getPostHistoryStore();
    const cookieList = getCookiesList();
    for (const key of (store.order || [])) {
      const item = store.items[key];
      if (!item) continue;
      if (String(item.type) !== 'thread') continue;
      const tid = String(item.id || item.threadId || '');
      if (tid !== String(threadId)) continue;
      const hash = String(item.userHash || '').trim();
      if (hash && findCookieByHash(cookieList, hash)) return hash;
    }
    return null;
  }
  // ── 初始化串内偏好 ──
  function initThreadCookiePref(threadId) {
    if (!threadId) return;
    // 直接检查原始 prefs(包括清除标记)
    const rawPref = getThreadCookiePrefs()[threadId];
    if (rawPref) {
      // 如果用户主动清除了偏好,不自动设置
      if (rawPref.cleared) return;
      if (rawPref.hash && findCookieByHash(getCookiesList(), rawPref.hash)) return; // 有效
      return; // 失效留给发送时处理
    }
    const poHash = detectPOCookieHash();
    console.log('[cookie-pref] init threadId=' + threadId + ' poHash=' + poHash);
    if (poHash) { setThreadCookiePref(threadId, poHash); refreshCookiePrefSwitchState(); return; }
    const histHash = detectHistoryCookieHash(threadId);
    console.log('[cookie-pref] init threadId=' + threadId + ' histHash=' + histHash);
    if (histHash) { setThreadCookiePref(threadId, histHash); refreshCookiePrefSwitchState(); return; }
  }
  // ── 注入"饼干偏好"开关到回应模式行 ──

  // 清空饼干偏好开关区域:无串上下文时不应残留上一个串的开关/齿轮/圆圈着色状态
  // 四处共用——发串模式、回复模式无串上下文、重置按钮、临时模式回复成功(漏一处即出现状态残留)
  function clearCookieCheckSwitch() {
    const area = document.querySelector('.xdex-cookie-check-area');
    if (area) area.innerHTML = '';
  }
  function injectCookieCheckSwitch(threadId) {
    // 注入到 .xdex-cookie-check-area(串内页和板块页共用)
    let area = document.querySelector('.xdex-cookie-check-area');
    // 串内页没有该区域时,在回应模式行的 uk-width-4-5 中创建
    if (!area) {
      // 优先找回应模式行内的输入区域,回退到第一个含回应模式标题的行
      const replyTitle = Array.from(document.querySelectorAll('.h-post-form-title')).find(el => el.textContent.trim() === '回应模式');
      const replyRow = (replyTitle ? replyTitle.closest('.h-post-form-grid') : null)?.querySelector('.h-post-form-input') || document.querySelector('.h-post-form-grid .h-post-form-input.uk-width-3-5');
      if (!replyRow) return;
      area = document.createElement('span');
      area.className = 'xdex-cookie-check-area';
      area.style.cssText = 'display:inline-flex;align-items:center;gap:2px;flex-shrink:0;margin-left:auto;';
      // 让父容器成为 flex 使串号和开关同行,空间分布
      // 扩展到 4/5 宽度,与板块页一致
      replyRow.classList.remove('uk-width-3-5');
      replyRow.classList.add('uk-width-4-5');
      replyRow.style.display = 'flex';
      replyRow.style.alignItems = 'center';
      // 串号 font 元素撑满,把 area 推到右侧
      const fontEl = replyRow.querySelector('font');
      if (fontEl) fontEl.style.flex = '1';
      replyRow.appendChild(area);
    }
    const pref = getThreadCookiePref(threadId);
    const isEnabled = !!pref;
    area.innerHTML = '<button type="button" class="xdex-edit-default-btn" title="修改默认饼干" style="visibility:' + (isEnabled ? 'visible' : 'hidden') + ';pointer-events:' + (isEnabled ? 'auto' : 'none') + ';">' + XDEX_ICON_GEAR + '</button>' +
      '<input type="checkbox" class="xdex-switch xdex-cookie-check-sw" role="switch" data-thread-id="' + threadId + '"' + (isEnabled ? ' checked' : '') + '>' +
      '<label style="font-size:11px;cursor:pointer;white-space:nowrap;">饼干偏好</label>';
    const switchEl = area.querySelector('.xdex-cookie-check-sw');
    const editBtn = area.querySelector('.xdex-edit-default-btn');
    switchEl.addEventListener('change', () => {
      if (switchEl.checked) {
        if (!getThreadCookiePref(threadId)) {
          // 智能选择默认饼干:PO主 > 发言历史 > 当前饼干
          const poHash = detectPOCookieHash();
          const histHash = detectHistoryCookieHash(threadId);
          const cur = getCurrentCookie();
          const defaultHash = poHash || histHash || (cur ? abbreviateName(cur.name) : '');
          if (defaultHash) setThreadCookiePref(threadId, defaultHash);
        }
        editBtn.style.visibility = 'visible';
        editBtn.style.pointerEvents = 'auto';
      } else {
        removeThreadCookiePref(threadId);
        editBtn.style.visibility = 'hidden';
        editBtn.style.pointerEvents = 'none';
      }
      refreshCookiePrefSwitchState();
    });
    editBtn.addEventListener('click', () => {
      showCookieConfirmDialog(threadId, (selectedHash) => {
        setThreadCookiePref(threadId, selectedHash);
        const _c = findCookieByHash(getCookiesList(), selectedHash);
        const _n = _c ? abbreviateName(_c.name || '').replace(/ - 0000-00-00 00:00:00$/g, '').trim() : selectedHash;
        toast('已将 ' + _n + ' 设为本串默认饼干');
        refreshCookiePrefSwitchState();
      }, () => {}, { mode: 'setDefault' });
    });
    // 注入/重建后立即按当前饼干与偏好的一致性着色
    refreshCookiePrefSwitchState();
  }

  /* --------------------------------------------------
   * tag 26. 阅图模式
   * -------------------------------------------------- */
  // ── 图片阅览器状态 ──
  const ImageViewer = {
    active: false,
    threadId: '',
    poHash: '',              // PO主的饼干has
    totalPageCount: 0,
    gridImages: [],          // {id, imgUrl, thumbUrl, isGif, userHash, name, title, content, contentRaw, now, page} // contentRaw: 原始HTML,用于详情页显示

    fetchedPages: new Set(), // 已请求过的页码(防重复请求,包含无图片的页)
    loadedPages: new Set(),  // 有图片的已加载页码(用于显示"已加载 X 页")
    loading: false,
    currentDetailIndex: -1,  // 当前详情查看的图片索引
    rotation: 0,
    scale: 1,
    panX: 0,
    panY: 0,
    gridScrollTop: 0,        // 返回瀑布流时恢复滚动位置
    // 向上翻页相关
    upwardEnabled: false,    // 向上翻页模式是否开启
    startPage: 0,            // 打开瀑布流时的起始页码
    _upwardRenderedCount: 0, // 向上容器已渲染的图片数
    renderedKeys: new Set(), // 已渲染到 DOM 的图片 id(防重复渲染)
  };
  function getImageViewerApiUrl(threadId, page) {
    return `https://api.nmb.best/api/thread?id=${encodeURIComponent(threadId)}&page=${encodeURIComponent(page)}`;
  }
  const XV_MAX_DISPLAY_HEIGHT = 500;

  function buildImageUrls(item) {
    const ext = (item.ext || '').toLowerCase();
    const isGif = ext === '.gif';
    const imgUrl = `https://image.nmb.best/image/${item.img}${item.ext}`;
    const thumbUrl = isGif ? imgUrl : `https://image.nmb.best/thumb/${item.img}${item.ext}`;
    return { imgUrl, thumbUrl, isGif };
  }
  // ── 预加载缩略图尺寸 ──
  function preloadThumbnailSizes() {
    const images = ImageViewer.gridImages;
    const toLoad = images.filter(img => !img._thumbW);
    if (!toLoad.length) return Promise.resolve();
    return Promise.allSettled(toLoad.map(img => new Promise((resolve) => {
      const probe = new Image();
      probe.onload = () => {
        img._thumbW = probe.naturalWidth || 200;
        img._thumbH = probe.naturalHeight || 200;
        // 计算在列宽下的显示高度
        const ratio = img._thumbH / img._thumbW;
        const cw = ImageViewer._colWidth || 200;
        let displayH = Math.round(cw * ratio);
        // 长图限高
        if (ratio > 3) {
          displayH = 500;
          img._isLong = true;
        }
        img._displayH = displayH;
        resolve();
      };
      probe.onerror = () => { img._thumbW = 200; img._thumbH = 200; img._displayH = 200; resolve(); };
      probe.src = img.thumbUrl;
    })));
  }
  function buildGridCard(img, index) {
    const card = document.createElement('div');
    card.className = 'xv-grid-card';
    card.dataset.imgId = img.id;
    card.style.cssText = 'border:1px solid var(--border,#333);border-radius:6px;overflow:hidden;cursor:pointer;background:var(--card,#1a1a1a);transition:border-color .15s;';
    const wrapper = document.createElement('div');
    wrapper.style.cssText = img._displayH
      ? 'position:relative;overflow:hidden;height:' + img._displayH + 'px;'
      : 'position:relative;overflow:hidden;';
    const imgEl = document.createElement('img');
    imgEl.src = img.thumbUrl;
    imgEl.loading = 'lazy';
    imgEl.style.cssText = img._displayH
      ? 'width:100%;height:100%;object-fit:cover;display:block;'
      : 'width:100%;height:auto;display:block;';
    wrapper.appendChild(imgEl);
    // GIF 角标
    if (img.isGif) {
      const gifBadge = document.createElement('span');
      gifBadge.style.cssText = 'position:absolute;top:4px;left:4px;background:rgba(0,0,0,.7);color:#0f0;font-size:10px;padding:1px 4px;border-radius:3px;z-index:1;';
      gifBadge.textContent = 'GIF';
      wrapper.appendChild(gifBadge);
    }
    // 长图角标(初始隐藏,onload 后判定)
    const longBadge = document.createElement('span');
    longBadge.style.cssText = 'position:absolute;' + (img.isGif ? 'top:20px;' : 'top:4px;') + 'left:4px;background:rgba(255,193,7,.85);color:#000;font-size:10px;padding:1px 4px;border-radius:3px;z-index:1;display:none;';
    longBadge.textContent = '长图';
    wrapper.appendChild(longBadge);
    card.appendChild(wrapper);
    const footer = document.createElement('div');
    footer.style.cssText = 'padding:4px 6px;display:flex;justify-content:space-between;align-items:center;';
    footer.innerHTML = `<span style="font-size:11px;color:var(--muted-foreground,#888);">No.${img.id}</span><span style="font-size:10px;color:var(--muted-foreground,#666);">P${img.page}·${img.pageIdx}</span>`;
    card.appendChild(footer);
    // 图片加载后:判定宽高比和长图,必要时切换原图、限高
    imgEl.addEventListener('load', function onLoad() {
      imgEl.removeEventListener('load', onLoad);
      const w = imgEl.naturalWidth || 1;
      const h = imgEl.naturalHeight || 1;
      const ratio = Math.max(w / h, h / w);
      // 宽高比 >3 的长图/宽图 → 限制最大高度,标记角标
      if (h > w && h / w > 3) {
        longBadge.style.display = '';
        wrapper.style.maxHeight = XV_MAX_DISPLAY_HEIGHT + 'px';
        imgEl.style.maxHeight = XV_MAX_DISPLAY_HEIGHT + 'px';
        imgEl.style.objectFit = 'cover';
      }
      // 缩略图太小或宽高比极端 → 换原图
      if (imgEl.naturalWidth < 60 || ratio > 3) {
        imgEl.src = img.imgUrl;
      }
    });
    card.addEventListener('click', () => {
      // 用图片 ID 查找当前索引(排序后旧 index 会失效)
      const idx = ImageViewer.gridImages.findIndex(g => g.id === img.id);
      openDetailLayer(idx >= 0 ? idx : index);
    });
    return card;
  }
  function stripHtml(html) {
    const tmp = document.createElement('div');
    tmp.innerHTML = html || '';
    return (tmp.textContent || '').trim();
  }
  async function fetchThreadPage(threadId, page) {
    const url = getImageViewerApiUrl(threadId, page);
    const resp = await gmRequest(url, 'json');
    const data = resp && (resp.response || resp.responseText);
    if (!data || typeof data === 'string') {
      try { return JSON.parse(data); } catch (e) { return null; }
    }
    return data;
  }
  async function loadViewerPages(threadId, startPage, count) {
    const pages = [];
    for (let i = 0; i < count; i++) {
      const p = startPage + i;
      if (ImageViewer.fetchedPages.has(p) || p < 1) continue;
      pages.push(p);
    }
    if (!pages.length) return;
    const results = await Promise.allSettled(
      pages.map(p => fetchThreadPage(threadId, p))
    );
    let appended = 0;
    results.forEach((r, i) => {
      const page = pages[i];
      if (r.status !== 'fulfilled' || !r.value) return;
      const thread = r.value;
      const replies = Array.isArray(thread.Replies) ? thread.Replies : [];
      // 记录PO主hash(从串的user_hash获取)
      if (!ImageViewer.poHash && thread.user_hash) ImageViewer.poHash = thread.user_hash;
      ImageViewer.fetchedPages.add(page); // 标记已请求
      let pageIdx = 0;
      replies.forEach(rep => {
        if (!rep || !rep.img || !rep.ext || rep.id === 9999999) return;
        const { imgUrl, thumbUrl, isGif } = buildImageUrls(rep);
        pageIdx++;
        ImageViewer.gridImages.push({
          id: rep.id,
          imgUrl,
          thumbUrl,
          isGif,
          userHash: rep.user_hash || '',
          name: rep.name || '',
          title: rep.title || '',
          content: stripHtml(rep.content || ''),
          contentRaw: rep.content || '',
          now: rep.now || '',
          page,
          pageIdx
        });
        appended++;
      });
      if (appended > 0) ImageViewer.loadedPages.add(page); // 只有含图片的页才计入已加载页数
      // 按页码+页内序号排序,保证 gridImages 始终是页码顺序(向上翻页加载的页面也能正确插入)
      ImageViewer.gridImages.sort((a, b) => a.page - b.page || (a.pageIdx || 0) - (b.pageIdx || 0));
      // 详情页打开时,sort 会改变索引,通过图片 ID 重新定位
      if (ImageViewer.currentDetailIndex >= 0 && ImageViewer._currentDetailImageId) {
        const newIdx = ImageViewer.gridImages.findIndex(g => g.id === ImageViewer._currentDetailImageId);
        if (newIdx >= 0) ImageViewer.currentDetailIndex = newIdx;
      }
      const replyCount = Number(thread.ReplyCount || replies.length);
      if (replyCount > 0) {
        const impliedTotal = Math.ceil(replyCount / 19);
        if (impliedTotal > ImageViewer.totalPageCount) ImageViewer.totalPageCount = impliedTotal;
      }
    });
    return appended;
  }
  // ── 入口:串内页添加"阅图"按钮 ──
  const IMAGE_VIEWER_TIPS_BTN_URL = 'https://file.moetu.org/images/2023/08/29/e1db89f8b15b0002b1ebdf574d5a7f30c6f2590891da1a8422eba303bf8b589c.gif';
  function ensureImageViewerButtonStyle() {
    if (document.getElementById('xdex-image-viewer-btn-style')) return;
    const style = document.createElement('style');
    style.id = 'xdex-image-viewer-btn-style';
    style.textContent = `
      .xdex-image-viewer-btn {
        position: fixed; top: 10px; right: 52px; z-index: 10000;
        width: 28px; height: 28px; padding: 0; margin: 0;
        border: none; border-radius: 50%;
        background: #99ffff; color: #000;
        font-size: 13px; font-weight: bold; cursor: pointer;
        box-shadow: 0 1px 4px rgba(0,0,0,.18);
        transition: transform .15s, box-shadow .15s, background .15s, filter .15s ease, opacity .15s ease;
        display: flex; align-items: center; justify-content: center;
        overflow: hidden; line-height: 1;
      }
      .xdex-image-viewer-btn:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0,0,0,.22); }
      .xdex-image-viewer-btn.xdex-iv-btn-hidden {
        display: none !important;
      }
      /* 与帖内模糊一致:默认模糊,悬停看清「图」字,点击不受影响 */
      .xdex-image-viewer-btn.xdex-iv-btn-blur {
        filter: blur(14px) brightness(0.5);
      }
      .xdex-image-viewer-btn.xdex-iv-btn-blur:hover {
        filter: none;
      }
      .xdex-image-viewer-btn.xdex-iv-btn-tips {
        background: #99ffff;
        color: transparent;
        font-size: 0;
      }
      .xdex-image-viewer-btn .xdex-iv-btn-tips-img {
        width: 100%; height: 100%;
        object-fit: cover; border-radius: 50%;
        pointer-events: none; display: block;
      }
      .xdex-image-viewer-btn svg {
        width: 18px; height: 18px;
        display: block; pointer-events: none;
      }
      #xdex-tombstone-mark {
        position: fixed; top: 44px; right: 52px; z-index: 10000;
        width: 28px; height: 28px; padding: 0; margin: 0;
        border: none; border-radius: 50%;
        background: #F0E0D6; color: #5b4636;
        cursor: pointer;
        box-shadow: 0 1px 4px rgba(0,0,0,.18);
        display: flex; align-items: center; justify-content: center;
        overflow: hidden; line-height: 1;
      }
      #xdex-tombstone-mark:hover {
        background: #e8d5c5; color: #c62828;
      }
      #xdex-tombstone-mark svg {
        width: 15px; height: 15px;
        display: block; pointer-events: none;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }
  // 阅图模式「分隔线」子设置:控制页码分隔线与“第N页”标识的显示(跨页面实时同步)
  function applyImageViewerSeparatorSetting(enabled) {
    let on = enabled;
    if (typeof on !== 'boolean') {
      try {
        on = !(typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state && SettingPanel.state.imageViewerSeparators === false);
      } catch (e) { on = true; }
    }
    const overlay = document.getElementById('xdex-image-viewer');
    if (!overlay) return;
    overlay.classList.toggle('xdex-iv-no-sep', !on);
  }
  function getCurrentImageHideModeForViewerBtn() {
    const live = window.__xdexImageHideMode;
    if (live && live !== 'none') return live;
    try {
      const cfg = (typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state)
        ? SettingPanel.state
        : (typeof SettingPanel !== 'undefined' && SettingPanel
          ? Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}))
          : null);
      const mode = cfg && cfg.applyImageHideMode;
      if (mode === 'blur' || mode === 'noimage' || mode === 'tips' || mode === 'default') return mode;
    } catch (e) {}
    return 'default';
  }
  function getCurrentThreadIdForImageViewer() {
    return PageType.getThreadId(false);
  }
  function isImageViewerThreadPage() {
    return PageType.isThreadPage(false);
  }
  function syncImageViewerButtonForHideMode(mode) {
    const IMAGE_VIEWER_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4"></rect><circle cx="9" cy="9" r="2"></circle><path d="M21 16l-4.6-4.6a2 2 0 0 0-2.8 0L5 20"></path></svg>';
    ensureImageViewerButtonStyle();
    const btn = document.getElementById('xdex-image-viewer-btn');
    if (!btn) return;
    const finalMode = (mode === 'blur' || mode === 'noimage' || mode === 'tips') ? mode : 'default';
    btn.classList.remove('xdex-iv-btn-hidden', 'xdex-iv-btn-blur', 'xdex-iv-btn-tips');
    const tipsImg = btn.querySelector('.xdex-iv-btn-tips-img');
    if (tipsImg) tipsImg.remove();
    // 恢复默认文字入口
    btn.innerHTML = IMAGE_VIEWER_ICON_SVG;
    btn.setAttribute('title', '阅图模式');
    btn.setAttribute('aria-label', '阅图模式');
    btn.style.display = '';
    if (finalMode === 'noimage') {
      // 隐藏但可恢复:仅加 class,不删节点
      btn.classList.add('xdex-iv-btn-hidden');
      btn.setAttribute('aria-hidden', 'true');
      return;
    }
    btn.removeAttribute('aria-hidden');
    if (finalMode === 'blur') {
      btn.classList.add('xdex-iv-btn-blur');
      return;
    }
    if (finalMode === 'tips') {
      btn.classList.add('xdex-iv-btn-tips');
      btn.textContent = '';
      const img = document.createElement('img');
      img.className = 'xdex-iv-btn-tips-img';
      img.src = IMAGE_VIEWER_TIPS_BTN_URL;
      img.alt = '阅图模式';
      img.draggable = false;
      btn.appendChild(img);
      btn.setAttribute('title', '阅图模式(Tips)');
      btn.setAttribute('aria-label', '阅图模式');
    }
  }
  function openImageViewerFromSettingsPanel() {
    if (!isImageViewerThreadPage()) {
      if (typeof toast === 'function') toast('请在串内页面使用阅图模式');
      return;
    }
    const threadId = getCurrentThreadIdForImageViewer();
    if (!threadId) {
      if (typeof toast === 'function') toast('未识别到当前串号');
      return;
    }
    // 无图模式下右上角入口隐藏,设置面板“打开”作为备选
    try {
      const $cover = $('#sp_cover');
      if ($cover.length && $cover.is(':visible')) $cover.fadeOut(120);
    } catch (e) {}
    openImageViewer(threadId);
  }
  function injectImageViewerButton() {
    if (!isImageViewerThreadPage()) return;
    ensureImageViewerButtonStyle();
    const threadId = getCurrentThreadIdForImageViewer();
    if (!threadId) return;
    let btn = document.getElementById('xdex-image-viewer-btn');
    if (!btn) {
      const IMAGE_VIEWER_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="4"></rect><circle cx="9" cy="9" r="2"></circle><path d="M21 16l-4.6-4.6a2 2 0 0 0-2.8 0L5 20"></path></svg>';
      const $btn = $('<button type="button" id="xdex-image-viewer-btn" class="xdex-image-viewer-btn" title="阅图模式" aria-label="阅图模式">').html(IMAGE_VIEWER_ICON_SVG);
      $btn.on('click', () => openImageViewer(threadId));
      $('body').append($btn);
      btn = $btn[0];
    }
    // 按当前隐藏模式同步外观(含无图隐藏可恢复)
    syncImageViewerButtonForHideMode(getCurrentImageHideModeForViewerBtn());
  }
  // ── 回收站墓碑标识: 浏览已被移入回收站的串时, 在阅图按钮下方展示垃圾桶标识 ──
  function injectThreadTombstoneMark() {
    const MARK_ID = 'xdex-tombstone-mark';
    let mark = document.getElementById(MARK_ID);
    if (!isImageViewerThreadPage()) { if (mark) mark.remove(); return; }
    const threadId = getCurrentThreadIdForImageViewer();
    if (!threadId) { if (mark) mark.remove(); return; }
    const tombs = getThreadHistoryStore().tombstones || {};
    const suffix = String(threadId).slice(0, 8);
    const candidates = ['normal:' + suffix, 'reply:' + suffix];
    let hitKey = null, tomb = null;
    for (const k of candidates) {
      if (tombs[k] && !tombs[k].purged && !tombs[k].revivedAt) { hitKey = k; tomb = tombs[k]; break; }
    }
    if (!tomb) { if (mark) mark.remove(); return; }
    if (!mark) {
      mark = document.createElement('button');
      mark.type = 'button';
      mark.id = MARK_ID;
      mark.innerHTML = XDEX_SVG_TRASH;
      mark.dataset.recycleKey = hitKey;
      mark.addEventListener('click', function (e) {
        e.preventDefault();
        const key = this.dataset.recycleKey || hitKey;
        try { openSettingsPanelModuleTab('history'); } catch (err) {}
        setTimeout(() => openRecycleBinAndLocate(key), 150);
      });
      document.body.appendChild(mark);
    }
    const daysLeft = Math.max(0, Math.ceil((Number(tomb.deletedAt) + THREAD_HISTORY_TOMBSTONE_TTL_MS - Date.now()) / 86400000));
    mark.title = `历史浏览数据在回收站中 · 约 ${daysLeft} 天后自动彻底清除 · 点击查看`;
    mark.setAttribute('aria-label', mark.title);
  }
  // ── 打开阅览器 ──
  function getImageViewerStartPage() {
    // 路径 /page/N 优先,再回退 ?page=(阅图语义)
    return PageType.getPathPageNum();
  }
  function renderGridImages() {
    const overlay = document.getElementById('xdex-image-viewer');
    if (!overlay) return;
    const info = overlay.querySelector('.xv-header-info');
    const masonry = overlay.querySelector('.xv-masonry');
    if (!masonry) return;
    const images = ImageViewer.gridImages;
    // 确保有一组列(只创建一次)
    let cols = Array.from(masonry.querySelectorAll('.xv-masonry-col'));
    if (cols.length === 0) {
      const { numCols, colWidth, gap } = getGridLayout();
      ImageViewer._colWidth = colWidth;
      ImageViewer._gap = gap;
      for (let c = 0; c < numCols; c++) {
        const col = document.createElement('div');
        col.className = 'xv-masonry-col';
        col.style.cssText = 'flex:0 0 ' + colWidth + 'px;display:flex;flex-direction:column;gap:' + gap + 'px;';
        masonry.appendChild(col);
        cols.push(col);
      }
    }
    // 找到最后一个已插入分隔线的页码
    let lastSepPage = 0;
    const existingSeps = masonry.querySelectorAll('.xv-page-separator');
    if (existingSeps.length > 0) {
      lastSepPage = parseInt(existingSeps[existingSeps.length - 1].dataset.page) || 0;
    }
    // 收集尚未渲染到 DOM 的新图片(用图片 id 去重,避免数组混杂时重复渲染)
    const newItems = [];
    for (let i = 0; i < images.length; i++) {
      if (!ImageViewer.renderedKeys.has(images[i].id)) {
        newItems.push({ img: images[i], index: i });
      }
    }
    const pageGroups = new Map();
    for (const item of newItems) {
      const p = item.img.page;
      if (!pageGroups.has(p)) pageGroups.set(p, []);
      pageGroups.get(p).push(item);
    }
    // 按页处理:LPT 全局最优分配
    const gap = 8;
    for (const [pageNum, pageItems] of pageGroups) {
      // 先插入分隔线(此时列高 = 上一页图片底部,第一页时 = 0)
      if (pageNum > lastSepPage) {
        const colHeights = cols.map(c => c.offsetHeight);
        masonry.appendChild(buildPageSeparator(pageNum, colHeights, cols[0].offsetWidth));
        lastSepPage = pageNum;
      }
      // 按显示高度降序排列(大图优先)
      pageItems.sort((a, b) => {
        const ha = a.img._displayH || 200;
        const hb = b.img._displayH || 200;
        return hb - ha;
      });
      // 用虚拟高度做全局分配,避免逐张贪心
      const vHeights = cols.map(c => c.offsetHeight);
      for (const item of pageItems) {
        let minIdx = 0;
        for (let c = 1; c < vHeights.length; c++) {
          if (vHeights[c] < vHeights[minIdx]) minIdx = c;
        }
        const card = buildGridCard(item.img, item.index);
        cols[minIdx].appendChild(card);
        ImageViewer.renderedKeys.add(item.img.id);  // 标记已渲染
        vHeights[minIdx] += (item.img._displayH || 200) + gap;
      }
    }
    info.textContent = '\u4e32\u53f7 ' + ImageViewer.threadId + ' \u00b7 \u5df2\u52a0\u8f7d ' + images.length + ' \u5f20\u56fe \u00b7 ' + ImageViewer.loadedPages.size + ' \u9875';
  }
  function openImageViewer(threadId) {
    if (ImageViewer.active) return;
    ImageViewer.active = true;
    ImageViewer.threadId = threadId;
    ImageViewer.poHash = '';
    ImageViewer.gridImages = [];
    ImageViewer.fetchedPages = new Set(); // 已请求过的页码(含无图片页)
    ImageViewer.loadedPages = new Set();  // 有图片的已加载页码
    ImageViewer.loading = false;
    ImageViewer.currentDetailIndex = -1;
    ImageViewer.totalPageCount = 0;
    ImageViewer.gridScrollTop = 0;
    ImageViewer.upwardEnabled = false;
    ImageViewer._upwardRenderedCount = 0;
    ImageViewer.renderedKeys.clear();
    ImageViewer.startPage = getImageViewerStartPage();
    ImageViewer._upwardResizeObserver = null;
    ImageViewer._pendingGridRender = false;
    ImageViewer._pendingUpwardPages = new Set(); // 详情页期间加载的向上页
    document.body.style.overflow = 'hidden';
    const startPage = getImageViewerStartPage();
    renderGridLayer();
    loadViewerPages(threadId, startPage, 2).then(async () => {
      await preloadThumbnailSizes();
      renderGridImages();
      // 图片太少撑不满视口时,自动继续加载直到填满或没有更多页
      const scroll = document.querySelector('.xv-grid-scroll');
      const maxIter = 20; // 防止无限循环
      for (let i = 0; i < maxIter; i++) {
        if (!scroll || scroll.scrollHeight > scroll.clientHeight + 10) break;
        const maxFetched = Math.max(...Array.from(ImageViewer.fetchedPages), 0);
        if (ImageViewer.totalPageCount > 0 && maxFetched >= ImageViewer.totalPageCount) break;
        await new Promise(r => setTimeout(r, 300)); // 限速,避免触发服务端风控
        const loaded = await loadViewerPages(threadId, maxFetched + 1, 2);
        if (!loaded) break;
        await preloadThumbnailSizes();
        renderGridImages();
      }
    });
  }
  // ── 关闭阅览器 ──
  function closeImageViewer() {
    ImageViewer.active = false;
    ImageViewer.threadId = '';
    ImageViewer.gridImages = [];
    ImageViewer.fetchedPages = new Set();
    ImageViewer.loadedPages = new Set();
    ImageViewer.currentDetailIndex = -1;
    ImageViewer.renderedKeys.clear();
    ImageViewer.rotation = 0;
    ImageViewer.scale = 1;
    ImageViewer.panX = 0;
    ImageViewer.panY = 0;
    if (ImageViewer._upwardResizeObserver) {
      ImageViewer._upwardResizeObserver.disconnect();
      ImageViewer._upwardResizeObserver = null;
    }
    document.body.style.overflow = '';
    const overlay = document.getElementById('xdex-image-viewer');
    if (overlay) overlay.remove();
    document.removeEventListener('keydown', viewerKeyHandler);
  }
  // ── 瀑布流层:骨架 ──
  function renderGridLayer() {
    const existing = document.getElementById('xdex-image-viewer');
    if (existing) existing.remove();
    const overlay = document.createElement('div');
    overlay.id = 'xdex-image-viewer';
    overlay.style.cssText = 'position:fixed;inset:0;z-index:10001;background:var(--background,#111);display:flex;flex-direction:column;overscroll-behavior:contain;';
    overlay.innerHTML = `
      <div class="xv-header" style="display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:var(--card,#1a1a1a);border-bottom:1px solid var(--border,#333);flex-shrink:0;">
        <span class="xv-header-info" style="color:var(--foreground,#ccc);font-size:13px;">串号 ${ImageViewer.threadId} · 正在加载…</span>
        <div style="display:flex;gap:6px;align-items:center;"><button class="xv-upward-btn" style="padding:4px 10px;background:#666;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;opacity:0.6;" title="开启向上翻页">▲</button><button class="xv-close-btn" style="padding:4px 10px;background:var(--destructive,#EE0000);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">退出</button></div>
      </div>
      <div class="xv-grid-scroll" style="flex:1;overflow-y:auto;overflow-anchor:none;padding:12px;">
        <div class="xv-masonry-upward" style="display:flex;gap:8px;align-items:flex-end;justify-content:center;position:relative;width:100%;"></div>
        <div class="xv-masonry-anchor" style="height:0;width:100%;flex-shrink:0;"></div>
        <div class="xv-masonry" style="display:flex;gap:8px;align-items:flex-start;justify-content:center;position:relative;width:100%;"></div>
      </div>
    `;
    document.body.appendChild(overlay);
    try { applyImageViewerSeparatorSetting(); } catch (e) {}
    // 拦截滚轮和触摸,防止穿透到原页面
    overlay.addEventListener('wheel', (e) => e.stopPropagation(), { passive: true });
    overlay.addEventListener('touchmove', (e) => e.stopPropagation(), { passive: true });
    overlay.querySelector('.xv-close-btn').addEventListener('click', closeImageViewer);
    const scroll = overlay.querySelector('.xv-grid-scroll');
    // 在真正滚动容器上拦截默认行为,防止滚到边界时穿透到底层页面
    scroll.addEventListener('wheel', (e) => {
      const atTop = scroll.scrollTop <= 0;
      const atBottom = scroll.scrollTop + scroll.clientHeight >= scroll.scrollHeight - 1;
      if ((e.deltaY < 0 && atTop) || (e.deltaY > 0 && atBottom)) {
        e.preventDefault();
      }
    }, { passive: false });
    scroll.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false });
    scroll.addEventListener('scroll', () => {
      if (ImageViewer.loading) return;
      // 向下加载:接近底部时触发
      if (scroll.scrollTop + scroll.clientHeight >= scroll.scrollHeight - 400) {
        loadNextGridPages();
      }
      // 向上加载:开启向上模式且接近顶部时触发
      if (ImageViewer.upwardEnabled && scroll.scrollTop <= 400) {
        loadPrevGridPages();
      }
    });
    // 向上翻页按钮事件
    const upwardBtn = overlay.querySelector('.xv-upward-btn');
    if (upwardBtn) {
      upwardBtn.addEventListener('click', () => {
        ImageViewer.upwardEnabled = !ImageViewer.upwardEnabled;
        if (ImageViewer.upwardEnabled) {
          upwardBtn.style.opacity = '1';
          upwardBtn.style.background = 'var(--primary,#006666)';
          upwardBtn.title = '关闭向上翻页';
          toast('已开启向上翻页');
          // 立即触发一次向上加载
          loadPrevGridPages();
        } else {
          upwardBtn.style.opacity = '0.6';
          upwardBtn.style.background = '#666';
          upwardBtn.title = '开启向上翻页';
          toast('已关闭向上翻页');
        }
      });
    }
  }
  // ── 瀑布流:获取或创建列容器 ──
  function getGridLayout() {
    const masonry = document.querySelector('.xv-masonry');
    if (!masonry) return { numCols: 4, colWidth: 200, gap: 8 };
    const containerWidth = masonry.clientWidth || 800;
    const gap = 8;
    const targetColWidth = 200;
    let numCols = Math.max(1, Math.floor((containerWidth + gap) / (targetColWidth + gap)));
    // 拉伸列宽填满容器,但限制在合理范围
    let colWidth = Math.floor((containerWidth - (numCols - 1) * gap) / numCols);
    colWidth = Math.max(160, Math.min(300, colWidth));
    return { numCols, colWidth, gap };
  }
  function pickShortestColumn(vHeights) {
    let minIdx = 0;
    for (let c = 1; c < vHeights.length; c++) {
      if (vHeights[c] < vHeights[minIdx]) minIdx = c;
    }
    return minIdx;
  }
  function pickUpwardGapColumn(vHeights, usedCols) {
    // 向上容器是 flex-end:列高越短,视觉顶部空白越大,少图页应优先补这些空白。
    const containerH = Math.max(...vHeights, 1);
    let bestIdx = -1;
    let bestGap = -Infinity;
    for (let c = 0; c < vHeights.length; c++) {
      if (usedCols && usedCols.has(c) && usedCols.size < vHeights.length) continue;
      const gap = containerH - vHeights[c];
      if (gap > bestGap) {
        bestGap = gap;
        bestIdx = c;
      }
    }
    if (bestIdx >= 0) return bestIdx;
    return pickShortestColumn(vHeights);
  }
  function buildPageSeparator(pageNum, colHeights, colWidth) {
    colWidth = colWidth || (ImageViewer._colWidth || 200);
    const gap = 8;
    const totalW = (colHeights && colHeights.length > 0)
      ? colHeights.length * colWidth + (colHeights.length - 1) * gap
      : colWidth;
    const sep = document.createElement('div');
    sep.className = 'xv-page-separator';
    sep.dataset.page = String(pageNum);
    if (!colHeights || colHeights.length < 2) {
      const minH = colHeights && colHeights.length ? Math.min(...colHeights) : 0;
      sep.style.cssText = 'position:absolute;left:50%;transform:translateX(-50%);width:' + totalW + 'px;top:' + minH + 'px;pointer-events:none;z-index:2;text-align:center;';
      sep.innerHTML = '<span style="display:inline-block;background:var(--background,#111);color:#EE0000;font-size:12px;font-weight:600;padding:2px 8px;">第' + pageNum + '页</span>';
      return sep;
    }
    const maxH = Math.max(...colHeights);
    const minH = Math.min(...colHeights);
    const contourH = Math.max(maxH - minH, 1);
    const points = [];
    for (let c = 0; c < colHeights.length; c++) {
      const x1 = c * (colWidth + gap);
      const x2 = x1 + colWidth;
      const y = colHeights[c] - minH;
      points.push(x1 + ',' + y);
      points.push(x2 + ',' + y);
    }
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('width', String(totalW));
    svg.setAttribute('height', String(contourH));
    svg.setAttribute('viewBox', '0 0 ' + totalW + ' ' + contourH);
    svg.setAttribute('preserveAspectRatio', 'none');
    svg.style.cssText = 'display:block;';
    const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
    polyline.setAttribute('points', points.join(' '));
    polyline.setAttribute('fill', 'none');
    polyline.setAttribute('stroke', '#EE0000');
    polyline.setAttribute('stroke-width', '2');
    polyline.setAttribute('vector-effect', 'non-scaling-stroke');
    svg.appendChild(polyline);
    sep.appendChild(svg);
    // label 放在 polyline 水平中点的实际路径上
    const midX = totalW / 2;
    let labelY = 0;
    for (let i = 0; i < points.length - 2; i += 2) {
      const x1 = parseFloat(points[i].split(',')[0]);
      const y1 = parseFloat(points[i].split(',')[1]);
      const x2 = parseFloat(points[i + 1].split(',')[0]);
      const y2 = parseFloat(points[i + 1].split(',')[1]);
      const x3 = parseFloat(points[i + 2].split(',')[0]);
      const y3 = parseFloat(points[i + 2].split(',')[1]);
      // 检查 midX 是否在 segment (x2,y2)→(x3,y3) 之间
      if (midX >= x2 && midX <= x3) {
        const t = (x3 !== x2) ? (midX - x2) / (x3 - x2) : 0;
        labelY = y2 + t * (y3 - y2);
        break;
      }
      // 检查 midX 是否在 segment (x1,y1)→(x2,y2) 之间
      if (midX >= x1 && midX <= x2) {
        const t = (x2 !== x1) ? (midX - x1) / (x2 - x1) : 0;
        labelY = y1 + t * (y2 - y1);
        break;
      }
    }
    const label = document.createElement('span');
    label.style.cssText = 'position:absolute;left:50%;top:' + Math.round(labelY) + 'px;transform:translate(-50%,-50%);background:var(--background,#111);color:#EE0000;font-size:12px;font-weight:600;padding:0 8px;white-space:nowrap;z-index:1;';
    label.textContent = '第' + pageNum + '页';
    sep.appendChild(label);
    // 绝对定位:top = 最矮列高度

    sep.style.cssText = 'position:absolute;left:50%;transform:translateX(-50%);width:' + totalW + 'px;top:' + minH + 'px;height:' + contourH + 'px;pointer-events:none;z-index:2;';

    return sep;
  }
  // ── 瀑布流层:渲染图片卡片(瀑布流,每页一组列) ──
  // ── 向上翻页:插入图片到瀑布流顶部 ──
  // ── 向上翻页:插入图片到独立的向上容器 ──
  function prependGridImages(pageNum) {
    const overlay = document.getElementById('xdex-image-viewer');
    if (!overlay) return;

    const upwardContainer = overlay.querySelector('.xv-masonry-upward');
    if (!upwardContainer) return;
    const images = ImageViewer.gridImages;
    const gap = 8;
    // 确保向上容器有一组列
    let cols = Array.from(upwardContainer.querySelectorAll('.xv-masonry-col'));
    if (cols.length === 0) {
      const { numCols, colWidth } = getGridLayout();
      ImageViewer._colWidth = colWidth; // 同步列宽,保证 SVG 等高线点位正确
      for (let c = 0; c < numCols; c++) {
        const col = document.createElement('div');
        col.className = 'xv-masonry-col';
        col.style.cssText = 'flex:0 0 ' + colWidth + 'px;display:flex;flex-direction:column;align-items:stretch;gap:' + gap + 'px;';
        upwardContainer.appendChild(col);
        cols.push(col);
      }
    }
    // 记录插入前列高度
    const colHeightsBefore = cols.map(c => c.offsetHeight);
    const containerHBefore = Math.max(...colHeightsBefore, 1);
    // console.log("[xv-pg] BEFORE insert", "containerHBefore=" + containerHBefore, "colHeights=" + JSON.stringify(colHeightsBefore));
    // 收集该页图片(过滤掉已渲染的,避免重复)
    const pageItems = images
      .map((img, index) => ({ img, index }))
      .filter(item => item.img.page === pageNum && !ImageViewer.renderedKeys.has(item.img.id));
    if (!pageItems.length) return 0; // 该页所有图片已渲染,直接跳过,避免 scrollTop += undefined
    // 按显示高度降序排列
    pageItems.sort((a, b) => (b.img._displayH || 200) - (a.img._displayH || 200));
    // LPT 分配到各列(插入到列最前面)
    // 用真实列高做 LPT 基准,这样新图片会优先填到最矮的列(考虑旧内容)
    const vHeights = cols.map(c => c.offsetHeight);
    const isSparseUpwardPage = pageItems.length < cols.length;
    const usedCols = new Set();
    for (const item of pageItems) {
      // 旧逻辑:所有向上页都按最短列 LPT 分配。少图页连续出现时,flex-end 顶部空白不会被优先补齐。
      // const minIdx = pickShortestColumn(vHeights);
      const minIdx = isSparseUpwardPage ? pickUpwardGapColumn(vHeights, usedCols) : pickShortestColumn(vHeights);
      const card = buildGridCard(item.img, item.index);
      cols[minIdx].insertBefore(card, cols[minIdx].firstChild);
      ImageViewer.renderedKeys.add(item.img.id);  // 标记已渲染
      vHeights[minIdx] += (item.img._displayH || 200) + gap;
      if (isSparseUpwardPage) usedCols.add(minIdx);
    }
    // 分隔线:标记第 pageNum 页的顶部等高线
    //
    // 上容器 flex-end 布局下,列顶部(视觉) = containerH - colHeight。
    // 每次新页插入到列头部后,旧内容整体下移,旧分隔线的 top 需要同步增加。
    // 关键性质:第 N 页图片在容器中的 y 坐标 = containerH_current - colHeight_after_insert_pageN,
    // 而这个值在后续页插入时只增加 delta = newContainerH - oldContainerH,polyline 形状不变。
    //
    // 计算插入后容器高度
    const uContainerH = Math.max(...cols.map(c => c.offsetHeight), 1);
    // console.log("[xv-pg] AFTER insert", "uContainerH=" + uContainerH, "delta=" + (uContainerH - containerHBefore), "colHeights=" + JSON.stringify(cols.map(c => c.offsetHeight)));
    // 更新所有已有分隔线:旧内容下移了 (uContainerH - containerHBefore) 像素
    const delta = uContainerH - containerHBefore;
    if (delta !== 0) {
      const oldSeps = upwardContainer.querySelectorAll('.xv-page-separator');
      oldSeps.forEach(sep => {
        const prevTop = parseFloat(sep.style.top) || 0;
        sep.style.top = (prevTop + delta) + 'px';
      });
    }
    // 当前页分隔线:该页首图顶部在容器中的 y 坐标 = uContainerH - 当前列高
    const sepHeights = cols.map(c => uContainerH - c.offsetHeight);
    const separator = buildPageSeparator(pageNum, sepHeights, cols[0].offsetWidth);
    // 绝对定位元素 append 到 positioned ancestor 根(而非某一列内部)
    upwardContainer.appendChild(separator);
    // 记录向上容器已渲染的图片数

    ImageViewer._upwardRenderedCount = upwardContainer.querySelectorAll('.xv-grid-card').length;
    return delta; // 返回向上容器高度增量,供调用方补偿滚动位置
    // 更新 header
    const info = overlay.querySelector('.xv-header-info');
    if (info) {
      info.textContent = '\u4e32\u53f7 ' + ImageViewer.threadId + ' \u00b7 \u5df2\u52a0\u8f7d ' + images.length + ' \u5f20\u56fe \u00b7 ' + ImageViewer.loadedPages.size + ' \u9875';
    }
  }
  // ── 向下加载下一页 ──
  async function loadNextGridPages() {
    if (ImageViewer.loading) return;
    ImageViewer.loading = true;
    const scroll = document.querySelector(".xv-grid-scroll");
    const maxIter = 20;
    for (let iter = 0; iter < maxIter; iter++) {
      const maxFetched = Math.max(...Array.from(ImageViewer.fetchedPages), 0);
      const nextPages = [];
      for (let i = 1; i <= 2; i++) {
        const p = maxFetched + i;
        if (!ImageViewer.fetchedPages.has(p)) nextPages.push(p);
      }
      if (!nextPages.length) break;
      if (iter > 0) await new Promise(r => setTimeout(r, 300)); // 限速
      await loadViewerPages(ImageViewer.threadId, nextPages[0], nextPages.length);
      await preloadThumbnailSizes();
      if (ImageViewer.currentDetailIndex >= 0) {
        ImageViewer._pendingGridRender = true;
      } else {
        renderGridImages();
      }
      // 内容撑满视口后停止自动加载,交给滚动事件触发
      if (scroll && scroll.scrollHeight > scroll.clientHeight + 10) break;
    }
    ImageViewer.loading = false;
  }
  // ── 向上加载上一页 ──
  async function loadPrevGridPages() {
    if (ImageViewer.loading) return;
    ImageViewer.loading = true;
    const scroll = document.querySelector(".xv-grid-scroll");
    const maxIter = 20;
    for (let iter = 0; iter < maxIter; iter++) {
      const minFetched = Math.min(...Array.from(ImageViewer.fetchedPages));
      if (minFetched <= 1) break;
      const prevPage = minFetched - 1;
      if (ImageViewer.fetchedPages.has(prevPage)) break;
      if (iter > 0) await new Promise(r => setTimeout(r, 300));
      await loadViewerPages(ImageViewer.threadId, prevPage, 1);
      await preloadThumbnailSizes();
      if (ImageViewer.currentDetailIndex >= 0) {
        ImageViewer._pendingUpwardPages.add(prevPage);
      } else {
        const upwardDelta = prependGridImages(prevPage);
        scroll.scrollTop += upwardDelta;
        if (ImageViewer._lastUpwardH !== undefined) {
          ImageViewer._lastUpwardH = scroll.querySelector(".xv-masonry-upward").offsetHeight;
        }
      }
      // 内容撑满视口后停止自动加载
      if (scroll && scroll.scrollHeight > scroll.clientHeight + 10) break;
    }
    // ResizeObserver 初始化(只执行一次)
    const upwardContainer = scroll.querySelector(".xv-masonry-upward");
    if (upwardContainer && !ImageViewer._upwardResizeObserver) {
      ImageViewer._lastUpwardH = upwardContainer.offsetHeight;
      ImageViewer._upwardResizeObserver = new ResizeObserver(() => {
        if (!ImageViewer.active) return;
        const newH = upwardContainer.offsetHeight;
        const dh = newH - ImageViewer._lastUpwardH;
        ImageViewer._lastUpwardH = newH;
        if (dh > 0) scroll.scrollTop += dh;
      });
      ImageViewer._upwardResizeObserver.observe(upwardContainer);
    }
    // 如果已经加载到第1页,自动关闭向上模式
    if (Math.min(...Array.from(ImageViewer.fetchedPages)) <= 1) {
      ImageViewer.upwardEnabled = false;
      const upwardBtn = document.querySelector(".xv-upward-btn");
      if (upwardBtn) {
        upwardBtn.style.opacity = "0.4";
        upwardBtn.style.background = "#444";
        upwardBtn.title = "已到第1页";
        upwardBtn.disabled = true;
      }
      toast("已加载到第1页");
    }
    ImageViewer.loading = false;
  }
  // ── 详情层:打开 ──
  function openDetailLayer(index) {
    ImageViewer.currentDetailIndex = index;
    ImageViewer._currentDetailImageId = ImageViewer.gridImages[index]?.id;
    ImageViewer.rotation = 0;
    ImageViewer.scale = 1;
    ImageViewer.panX = 0;
    ImageViewer.panY = 0;
    ImageViewer.gridScrollTop = document.querySelector('.xv-grid-scroll')?.scrollTop || 0;
    renderDetailLayer();
    document.addEventListener('keydown', viewerKeyHandler);
    prefetchAdjacentImages(index);
  }
  // ── 详情层:关闭回到瀑布流 ──
  function closeDetailLayer() {
    // 保存当前图片 ID(用于返回瀑布流后滚动定位)
    const _returnToImageId = ImageViewer._currentDetailImageId;
    ImageViewer.currentDetailIndex = -1;
    document.removeEventListener('keydown', viewerKeyHandler);
    // 移除引用弹窗层级修复样式
    const zfix = document.getElementById('xdex-detail-quote-zfix');
    if (zfix) zfix.remove();
    const overlay = document.getElementById('xdex-image-viewer');
    if (!overlay) return;
    const detail = overlay.querySelector('.xv-detail');
    if (detail) detail.remove();
    const grid = overlay.querySelector('.xv-grid-scroll');
    if (grid) {
      grid.style.display = '';
      setTimeout(() => {
        // 补渲染:详情页期间加载的新页面现在可以正确测量列高
        if (ImageViewer._pendingGridRender) {
          ImageViewer._pendingGridRender = false;
          renderGridImages();
        }
        // 补渲染:详情页期间加载的向上页面
        if (ImageViewer._pendingUpwardPages.size > 0) {
          const pending = Array.from(ImageViewer._pendingUpwardPages).sort((a, b) => b - a);
          ImageViewer._pendingUpwardPages.clear();
          for (const p of pending) prependGridImages(p);
        }
        // 滚动到当前查看图片的卡片位置
        if (_returnToImageId) {
          const card = grid.querySelector('[data-img-id="' + _returnToImageId + '"]');
          if (card) {
            card.scrollIntoView({ block: 'center', behavior: 'instant' });
            // 临时高亮 2 秒,方便定位
            card.style.boxShadow = '0 0 0 3px #00ffccb3, 0 0 8px #66ccff66, 0 0 16px #99ffff40';
            card.style.transition = 'box-shadow 0.6s ease';
            setTimeout(() => { card.style.boxShadow = ''; }, 2000);
          }
          else { grid.scrollTop = ImageViewer.gridScrollTop; }
        } else { grid.scrollTop = ImageViewer.gridScrollTop; }
      }, 0);
    }
    const header = overlay.querySelector('.xv-header');
    if (header) header.style.display = 'flex';
  }
  // ── 详情层:渲染 ──
  function renderDetailLayer() {
    const overlay = document.getElementById('xdex-image-viewer');
    if (!overlay) return;
    const grid = overlay.querySelector('.xv-grid-scroll');
    if (grid) grid.style.display = 'none';
    const header = overlay.querySelector('.xv-header');
    if (header) header.style.display = 'none';
    let detail = overlay.querySelector('.xv-detail');
    if (!detail) {
      detail = document.createElement('div');
      detail.className = 'xv-detail';
      detail.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;background:var(--background,#111);';
      overlay.appendChild(detail);
    }
    const img = ImageViewer.gridImages[ImageViewer.currentDetailIndex];
    if (!img) { closeDetailLayer(); return; }
    detail.innerHTML = `
      <div style="display:flex;align-items:center;justify-content:space-between;padding:8px 16px;background:var(--card,#1a1a1a);border-bottom:1px solid var(--border,#333);flex-shrink:0;">
        <button class="xv-back-btn" style="padding:4px 10px;background:var(--primary,#006666);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">返回瀑布流</button>
        <span style="color:var(--foreground,#ccc);font-size:13px;">${ImageViewer.currentDetailIndex + 1} / ${ImageViewer.gridImages.length} · No.${img.id} · 第${img.page}页 · 第${img.pageIdx}/${ImageViewer.gridImages.reduce((m, g) => g.page === img.page && g.pageIdx > m ? g.pageIdx : m, 0)}张</span>
        <button class="xv-close-btn2" style="padding:4px 10px;background:var(--destructive,#EE0000);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;">退出</button>
      </div>
      <div style="display:flex;flex:1;overflow:hidden;">
        <div style="flex:1;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;">
          <button class="xv-prev-btn" style="position:absolute;left:8px;top:50%;transform:translateY(-50%);width:36px;height:36px;background:rgba(0,0,0,.5);color:#fff;border:none;border-radius:50%;cursor:pointer;font-size:18px;z-index:2;">◀</button>
          <img class="xv-main-img" src="${img.imgUrl}" style="max-width:90%;max-height:90%;object-fit:contain;transition:transform .15s;cursor:grab;" draggable="false">
          <button class="xv-next-btn" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);width:36px;height:36px;background:rgba(0,0,0,.5);color:#fff;border:none;border-radius:50%;cursor:pointer;font-size:18px;z-index:2;">▶</button>
        </div>
        <div class="h-threads-info" style="width:240px;border-left:1px solid var(--border,#333);padding:12px;overflow-y:auto;background:var(--card,#1a1a1a);flex-shrink:0;font-size:13px;line-height:1.6;">
          ${img.title && img.title !== '无标题' ? '<span class="h-threads-info-title" style="font-weight:bold;margin-right:6px;">' + escapeHtml(img.title) + '</span>' : ''}
          ${img.name && img.name !== '无名氏' ? '<span class="h-threads-info-email" style="color:var(--primary,#6cf);margin-right:6px;">' + escapeHtml(img.name) + '</span>' : ''}
          <span class="h-threads-info-createdat" style="color:var(--muted-foreground,#888);font-size:12px;">${escapeHtml(img.now)}</span>
          <br>
          ${img.userHash ? '<span class="h-threads-info-uid" style="color:var(--foreground,#ccc);">ID:' + escapeHtml(img.userHash) + '</span>' : ''}${img.userHash && img.userHash === ImageViewer.poHash ? ' <span class="uk-text-primary uk-text-small">(PO主)</span>' : ''}
          <a href="https://www.nmbxd1.com/t/${ImageViewer.threadId}?r=${img.id}" class="h-threads-info-id" target="_blank" rel="noopener" style="margin-left:8px;color:var(--primary,#6cf);font-size:12px;">No.${img.id}</a>
          ${(img.contentRaw && img.contentRaw !== '分享图片') ? '<div class="h-threads-content h-threads-info" style="margin-top:8px;color:var(--foreground,#ddd);line-height:1.5;">' + img.contentRaw + '</div>' : ''}
        </div>
      </div>
      <div style="display:flex;align-items:center;justify-content:center;gap:6px;padding:8px;background:var(--card,#1a1a1a);border-top:1px solid var(--border,#333);flex-shrink:0;">
        <button class="xv-rotate-left" style="padding:4px 8px;font-size:12px;">左旋</button>
        <button class="xv-rotate-right" style="padding:4px 8px;font-size:12px;">右旋</button>
        <button class="xv-zoom-in" style="padding:4px 8px;font-size:12px;">放大</button>
        <button class="xv-zoom-out" style="padding:4px 8px;font-size:12px;">缩小</button>
        <button class="xv-reset" style="padding:4px 8px;font-size:12px;">复位</button>
      </div>
    `;
    // 绑定事件
    detail.querySelector('.xv-back-btn').addEventListener('click', closeDetailLayer);
    detail.querySelector('.xv-close-btn2').addEventListener('click', closeImageViewer);
    detail.querySelector('.xv-prev-btn').addEventListener('click', () => navigateDetail(-1));
    detail.querySelector('.xv-next-btn').addEventListener('click', () => navigateDetail(1));
    detail.querySelector('.xv-rotate-left').addEventListener('click', () => adjustRotation(-90));
    detail.querySelector('.xv-rotate-right').addEventListener('click', () => adjustRotation(90));
    detail.querySelector('.xv-zoom-in').addEventListener('click', () => adjustScale(0.2));
    detail.querySelector('.xv-zoom-out').addEventListener('click', () => adjustScale(-0.2));
    detail.querySelector('.xv-reset').addEventListener('click', resetTransform);
    // 拖拽平移图片
    const mainImg = detail.querySelector('.xv-main-img');
    let dragStartX = 0, dragStartY = 0, dragMoved = false;
    mainImg.addEventListener('mousedown', (e) => {
      e.preventDefault();
      dragStartX = e.clientX;
      dragStartY = e.clientY;
      dragMoved = false;
      mainImg.style.cursor = 'grabbing';
      const onMove = (ev) => {
        const dx = ev.clientX - dragStartX;
        const dy = ev.clientY - dragStartY;
        if (Math.abs(dx) > 3 || Math.abs(dy) > 3) dragMoved = true;
        // scale 影响平移量的感知,除以 scale 让拖拽距离与视觉一致
        ImageViewer.panX += (ev.clientX - dragStartX) / ImageViewer.scale;
        ImageViewer.panY += (ev.clientY - dragStartY) / ImageViewer.scale;
        dragStartX = ev.clientX;
        dragStartY = ev.clientY;
        applyTransform();
      };
      const onUp = () => {
        mainImg.style.cursor = 'grab';
        window.removeEventListener('mousemove', onMove);
        window.removeEventListener('mouseup', onUp);
        // 没有拖拽 → 当作点击,左 30% 上一张,右 30% 下一张
        if (!dragMoved) {
          const rect = mainImg.getBoundingClientRect();
          const x = e.clientX - rect.left;
          if (x < rect.width * 0.3) navigateDetail(-1);
          else if (x > rect.width * 0.7) navigateDetail(1);
        }
      };
      window.addEventListener('mousemove', onMove);
      window.addEventListener('mouseup', onUp);
    });
    // 滚轮缩放
    const imgContainer = detail.querySelector(".xv-main-img");
    if (imgContainer) {
      imgContainer.addEventListener("wheel", (e) => {
        e.preventDefault();
        adjustScale(e.deltaY < 0 ? 0.15 : -0.15);
      }, { passive: false });
    }
    applyTransform();
    // 提升引用弹窗层级(覆盖详情层 z-index:10001)
    if (!document.getElementById('xdex-detail-quote-zfix')) {
      const zfix = document.createElement('style');
      zfix.id = 'xdex-detail-quote-zfix';
      // 详情层(10001)与导出串弹窗(10001)之上:遮罩 10006、窗体/关闭钮 10007、原生框 20000 已够
      zfix.textContent = '#h-ref-view{z-index:10007!important}.qp-overlay-quote{z-index:10006!important}.qp-close-all{z-index:10007!important}.qp-stack{z-index:10007!important}';
      document.head.appendChild(zfix);
    }
    // 标记饼干
    try { if (typeof markAllCookies === 'function') markAllCookies(getFilterConfig().markedGroups || [], detail); } catch (e) {}
    // 选择性增强:顺序很重要
    // 1. 先链接化URL(纯文本 → <a> 标签)
    try {
      const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      if (cfg && cfg.enableAutoUrlLinkify && typeof runAutoUrlLinkify === 'function') runAutoUrlLinkify(detail);
    } catch (e) {}
    // 2. 再拓展引用格式(裸数字 → <font> 标签,必须在URL链接化之后,避免破坏URL结构)
    try { if (typeof extendQuote === 'function') extendQuote(detail); } catch (e) {}
    // 3. 最后挂引用弹窗处理器(必须在extendQuote之后,才能覆盖新创建的<font>标签)
    try { if (typeof initExtendedContent === 'function') { console.log('[xv-detail] calling initExtendedContent, fonts:', detail.querySelectorAll("font[color='#789922']").length); initExtendedContent(detail); console.log('[xv-detail] initExtendedContent done'); } } catch (e) { console.error('[xv-detail] initExtendedContent error:', e); }

  }
  function decodeHtmlEntities(text) {
    const ta = document.createElement('textarea');
    ta.innerHTML = text || '';
    return ta.value;
  }
  // ── 详情层:导航 ──
  function navigateDetail(delta) {
    const next = ImageViewer.currentDetailIndex + delta;
    // 向左超过数组开头 → 加载上一页并跳到其最后一张
    if (next < 0) {
      if (ImageViewer.loading) return;
      const minLoaded = Math.min(...Array.from(ImageViewer.loadedPages));
      if (minLoaded <= 1) { toast('已经到顶♂了……'); return; }
      ImageViewer.loading = true;
      const targetPage = minLoaded - 1;
      loadViewerPages(ImageViewer.threadId, targetPage, 1).then(async () => {
        await preloadThumbnailSizes();
        let lastIdx = -1;
        for (let i = ImageViewer.gridImages.length - 1; i >= 0; i--) {
          if (ImageViewer.gridImages[i].page === targetPage) { lastIdx = i; break; }
        }
        if (lastIdx >= 0) {
          ImageViewer.currentDetailIndex = lastIdx;
          ImageViewer._currentDetailImageId = ImageViewer.gridImages[lastIdx]?.id;
          renderDetailLayer();
          prefetchAdjacentImages(lastIdx);
        }
        if (lastIdx < 0) toast('已是第一页');
        ImageViewer.loading = false;
      });
      return;
    }
    // 向右超过数组末尾 → 加载下一页并跳到其第一张
    if (next >= ImageViewer.gridImages.length) {
      if (ImageViewer.loading) return;
      const maxLoaded = Math.max(...Array.from(ImageViewer.loadedPages), 0);
      ImageViewer.loading = true;
      const targetPage = maxLoaded + 1;
      loadViewerPages(ImageViewer.threadId, targetPage, 1).then(async () => {
        await preloadThumbnailSizes();
        const firstIdx = ImageViewer.gridImages.findIndex(g => g.page === targetPage);
        if (firstIdx >= 0) {
          ImageViewer._currentDetailImageId = ImageViewer.gridImages[firstIdx]?.id;
          ImageViewer.currentDetailIndex = firstIdx;
          renderDetailLayer();
          prefetchAdjacentImages(firstIdx);
        }
        if (firstIdx < 0) toast('已经到底♂了……');
        ImageViewer.loading = false;
      });
      return;
    }
    // 正常导航
    ImageViewer.currentDetailIndex = next;
    ImageViewer._currentDetailImageId = ImageViewer.gridImages[next]?.id;
    ImageViewer.rotation = 0;
    ImageViewer.scale = 1;
    ImageViewer.panX = 0;
    ImageViewer.panY = 0;
    renderDetailLayer();
    prefetchAdjacentImages(next);
    // 预加载相邻页
    if (next >= ImageViewer.gridImages.length - 3) {
      loadNextGridPages();
    } else if (next <= 2) {
      loadPrevGridPages();
    }
  }
  // ── 图片变换 ──
  function applyTransform() {
    const img = document.querySelector('.xv-main-img');
    if (!img) return;
    img.style.transform = `rotate(${ImageViewer.rotation}deg) scale(${ImageViewer.scale}) translate(${ImageViewer.panX}px, ${ImageViewer.panY}px)`;
  }
  function adjustRotation(deg) {
    ImageViewer.rotation = (ImageViewer.rotation + deg) % 360;
    applyTransform();
  }
  function adjustScale(delta) {
    ImageViewer.scale = Math.max(0.01, ImageViewer.scale + delta);
    applyTransform();
  }
  function resetTransform() {
    ImageViewer.rotation = 0;
    ImageViewer.scale = 1;
    ImageViewer.panX = 0;
    ImageViewer.panY = 0;
    applyTransform();
  }
  // ── 预加载相邻图片 ──
  function prefetchAdjacentImages(index) {
    [-1, 1].forEach(offset => {
      const target = ImageViewer.gridImages[index + offset];
      if (!target) return;
      const img = new Image();
      img.src = target.imgUrl;
    });
  }
  // ── 键盘处理 ──
  function viewerKeyHandler(e) {
    if (!ImageViewer.active || ImageViewer.currentDetailIndex < 0) return;
    switch (e.key) {
      case 'ArrowLeft':
        e.preventDefault();
        navigateDetail(-1);
        break;
      case 'ArrowRight':
        e.preventDefault();
        navigateDetail(1);
        break;
      case 'ArrowUp':
        e.preventDefault();
        ImageViewer.panY += 60;
        applyTransform();
        break;
      case 'ArrowDown':
        e.preventDefault();
        ImageViewer.panY -= 60;
        applyTransform();
        break;
      case '[':
        adjustRotation(-90);
        break;
      case ']':
        adjustRotation(90);
        break;
      case '=':
      case '+':
        adjustScale(0.2);
        break;
      case '-':
        adjustScale(-0.2);
        break;
      case '0':
        resetTransform();
        break;
    }
  }

  /* --------------------------------------------------
   * tag 27. 关闭引用
   * -------------------------------------------------- */
  // 阻止URL中r=参数导致的自动引用插入(网站原生行为)
  // 开关:设置面板 → 关闭引用(disableAutoQuote),实时生效
  // 方案 B:仍拦截自动插入,但用户手动点串号时短暂放行
  let xdexManualQuoteBypass = null; // { rid: string|null, until: number }
  function allowManualQuoteInsert(rid, ttlMs = 2000) {
    xdexManualQuoteBypass = {
      rid: rid != null && rid !== '' ? String(rid) : null,
      until: Date.now() + (Number(ttlMs) > 0 ? Number(ttlMs) : 2000)
    };
  }
  function shouldBypassAutoQuoteStrip(value, quoteRe, rParam) {
    const state = xdexManualQuoteBypass;
    if (!state) return false;
    if (Date.now() > state.until) {
      xdexManualQuoteBypass = null;
      return false;
    }
    // 未指定 rid:放行任何手动写入;指定了则仅当写入内容匹配对应引用时放行
    if (!state.rid) return true;
    if (String(state.rid) === String(rParam || '') && quoteRe && quoteRe.test(String(value || ''))) return true;
    try {
      const esc = String(state.rid).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      return new RegExp('^\\s*>>No\\.' + esc + '\\s*\\n?').test(String(value || ''));
    } catch (e) {
      return false;
    }
  }
  function extractQuoteIdFromClickTarget(target) {
    if (!target || !target.closest) return '';
    // 发言预览的占位编号不参与自动引用放行(避免 No.占位号 误设 2 秒手动引用标记)
    if (target.closest('.h-preview-box')) return '';
    const a = target.closest('a.h-threads-info-id, a[href*="r="], a[href*="/t/"]');
    if (!a) return '';
    // 仅关注串号/引用链接,避免误伤普通导航
    const href = a.getAttribute('href') || '';
    try {
      const url = new URL(href, location.origin);
      const rid = url.searchParams.get('r');
      if (rid) return String(rid);
    } catch (e) {}
    const textMatch = String(a.textContent || '').match(/No\.(\d{4,8})/i);
    if (textMatch) return textMatch[1];
    const hrefMatch = href.match(/(?:[?&]r=|No\.)(\d{4,8})/i);
    return hrefMatch ? hrefMatch[1] : '';
  }
  // ★ ?page=N&r=ID 链接定位:打开页面时滚动到 r= 对应回复并尽量在视野中居中。
// 独立于「关闭引用」开关(开关只控制剥引用,定位是浏览体验)。
// 判定用 data-threads-id(r= 号即回复号),不依赖站点的 selected 标记时序;
// 图片/相对时间等增强会改变高度,定位后二次校准。
function initReplyParamScrollTo() {
    try {
      const params = new URLSearchParams(location.search);
      const rParam = params.get('r');
      if (!rParam || params.get('page') === null || !/^\d{6,8}$/.test(rParam)) return;
      const scrollTarget = () => document.querySelector('[data-threads-id="' + rParam + '"]');
      let attempts = 0;
      const tryScroll = () => {
        const el = scrollTarget();
        if (el) {
          el.scrollIntoView({ block: 'center', behavior: 'auto' });
          // 二次校准:图片/懒加载/字体就绪后高度变化会偏离居中
          setTimeout(() => {
            try {
              if (el.isConnected) el.scrollIntoView({ block: 'center', behavior: 'auto' });
            } catch (e) {}
          }, 800);
          return;
        }
        if (++attempts <= 20) setTimeout(tryScroll, 300);
      };
      tryScroll();
    } catch (e) { /* 定位失败不影响页面 */ }
  }
  function initDisableAutoQuote() {
    const rParam = new URLSearchParams(location.search).get('r');
    if (!rParam) return;
    const rEsc = rParam.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const quoteRe = new RegExp('^\\s*>>No\\.' + rEsc + '\\s*\\n?');
    // 读取实时设置(支持面板切换后即时生效)
    function isEnabled() {
      try {
        const s = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
        return s.disableAutoQuote !== false; // 默认 true(开启拦截)
      } catch (e) { return true; }
    }
    // 值班室/举报表单需要保留 r= 自动引用,以便精准举报
    function shouldPreserveAutoQuoteForReport() {
      try {
        const decodedPath = decodeURIComponent(location.pathname || '');
        if (/^\/f\/值班室(?:\/|$)/.test(decodedPath)) return true;
      } catch (e) {}
      try {
        return !!document.querySelector('form[action="/Home/Forum/doReplyThread.html"] #h-report-switch, form[action="/Home/Forum/doPostThread.html"] #h-report-switch, #h-report-switch');
      } catch (e) {
        return false;
      }
    }
    // 检查草稿是否本身以该引用号开头(如果是,说明引用来自草稿而非网站自动插入)
    function draftHasQuote() {
      try {
        if (typeof readDraftValue === 'function') {
          const draft = readDraftValue();
          return typeof draft === 'string' && quoteRe.test(draft);
        }
      } catch (e) {}
      return false;
    }
    function shouldStripAutoQuote(v) {
      if (!isEnabled() || draftHasQuote() || shouldPreserveAutoQuoteForReport()) return false;
      if (!quoteRe.test(String(v || ''))) return false;
      // 用户手动点击/手动追记:短暂放行
      if (shouldBypassAutoQuoteStrip(v, quoteRe, rParam)) return false;
      return true;
    }
    function interceptTextarea(ta) {
      if (ta.__quoteIntercepted) return;
      ta.__quoteIntercepted = true;
      const desc = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value');
      Object.defineProperty(ta, 'value', {
        configurable: true, enumerable: true,
        get() { return desc.get.call(this); },
        set(v) {
          // 仅剥离 URL r= 导致的自动引用;手动点串号追记不拦
          if (shouldStripAutoQuote(v)) {
            v = String(v).replace(quoteRe, '');
          }
          desc.set.call(this, v);
        }
      });
      // 初始清理(仅在开启且草稿不含该引用且非值班室/举报场景时)
      if (shouldStripAutoQuote(ta.value)) {
        const cleaned = String(ta.value).replace(quoteRe, '');
        desc.set.call(ta, cleaned);
        ta.dispatchEvent(new Event('input', { bubbles: true }));
      }
    }
    // 捕获阶段监听点击:先于站点/自己的写入逻辑设好放行标记
    if (!initDisableAutoQuote.__manualClickBound) {
      initDisableAutoQuote.__manualClickBound = true;
      document.addEventListener('click', (e) => {
        if (e.button != null && e.button !== 0) return;
        const rid = extractQuoteIdFromClickTarget(e.target);
        if (!rid) return;
        // 只需要放行与 URL r= 相同的那条(其他 No. 本来就不会被剥)
        if (String(rid) === String(rParam)) allowManualQuoteInsert(rid);
      }, true);
    }
    const obs = new MutationObserver(() => {
      const ta = document.querySelector('textarea.h-post-form-textarea');
      if (ta) { interceptTextarea(ta); obs.disconnect(); }
    });
    obs.observe(document.body, { childList: true, subtree: true });
    setTimeout(() => { const ta = document.querySelector('textarea.h-post-form-textarea'); if (ta) interceptTextarea(ta); }, 1000);
  }

  /* --------------------------------------------------
   * tag 28. 值班室自动选择理由
   * -------------------------------------------------- */
  // 举报理由:自动补充并默认选择“其他”,避免值班室/举报表单未选理由时阻止发送
  function autoSelectReportReason(root = document) {
    const scope = root && root.querySelectorAll ? root : document;
    const selects = [];
    if (scope.matches && scope.matches('select#h-report-switch')) selects.push(scope);
    scope.querySelectorAll?.('select#h-report-switch').forEach(sel => selects.push(sel));
    selects.forEach(sel => {
      if (sel.__xdexReportReasonHandled) return;
      sel.__xdexReportReasonHandled = true;
      const customValue = '其他';
      let option = Array.from(sel.options || []).find(opt => opt.value === customValue || opt.textContent.trim() === customValue);
      if (!option) {
        option = document.createElement('option');
        option.value = customValue;
        option.textContent = customValue;
        sel.appendChild(option);
      }
      if (!sel.value || sel.value === '-1') {
        sel.value = option.value;
        sel.dispatchEvent(new Event('change', { bubbles: true }));
      }
      const form = sel.closest('form');
      const info = (form || document).querySelector('#h-report-info');
      if (info && sel.value && sel.value !== '-1') info.style.display = 'none';
    });
    if (!autoSelectReportReason.__observerInstalled && root === document && document.body) {
      autoSelectReportReason.__observerInstalled = true;
      const observer = new MutationObserver(mutations => {
        mutations.forEach(m => {
          m.addedNodes.forEach(node => {
            if (node && node.nodeType === 1) autoSelectReportReason(node);
          });
        });
      });
      observer.observe(document.body, { childList: true, subtree: true });
    }
  }

  /* --------------------------------------------------
   * tag 29. WebDAV 备份/同步
   * -------------------------------------------------- */
  const WEBDAV_CONFIG_KEY = 'xdex_webdav_config';
  const WEBDAV_SYNC_FILE = 'xdex-webdav-sync.json';
  // 自动同步:跨页面共享计时器(localStorage),间隔 1 小时;到点未同步则新页面立即补一次;手动同步会重置计时器
  const WEBDAV_AUTO_INTERVAL_MS = 60 * 60 * 1000;
  const WEBDAV_AUTO_NEXT_KEY = 'xdex_webdav_auto_next_ts';
  const WEBDAV_AUTO_RUNNING_KEY = 'xdex_webdav_auto_running_ts';
  const WEBDAV_AUTO_LAST_FP_KEY = 'xdex_webdav_last_fp';
  const WEBDAV_AUTO_LAST_IMMEDIATE_KEY = 'xdex_webdav_last_immediate_ts';
  // 设置冲突挂起:手动同步选择「取消本次同步」后置位;自动同步遇到差异时跳过设置部分,直到手动处理
  const WEBDAV_SETTINGS_PENDING_KEY = 'xdex_webdav_settings_pending';
  // 开启开关触发的立即同步:60s 冷却 + 内容指纹防重
  const WEBDAV_IMMEDIATE_COOLDOWN_MS = 60 * 1000;
  function webdavAutoGet(key) {
    try { const v = localStorage.getItem(key); return v == null ? '' : String(v); } catch (e) { return ''; }
  }
  function webdavFnv1a(str) {
    let h = 0x811c9dc5;
    for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = (h * 0x01000193) >>> 0; }
    return h;
  }
  // 内容指纹:剔除自变机制字段(lastSyncAt / lastUpdatedAt),保留反映真实活动的数据
  function webdavFingerprint(payload) {
    try {
      const copy = JSON.parse(JSON.stringify(payload || {}));
      const strip = (v) => {
        if (v === null || typeof v !== 'object') return;
        Object.keys(v).forEach((k) => {
          if (k === 'lastSyncAt' || k === 'lastUpdatedAt') {
            delete v[k];
          } else if (v[k] !== null && typeof v[k] === 'object') {
            strip(v[k]);
          }
        });
      };
      strip(copy);
      return String(webdavFnv1a(JSON.stringify(copy)));
    } catch (e) {
      return '';
    }
  }
  function webdavAutoNow() {
    return Date.now();
  }
  function webdavAutoRead(key) {
    try { return Number(localStorage.getItem(key)) || 0; } catch (e) { return 0; }
  }
  function webdavAutoWrite(key, ts) {
    try { localStorage.setItem(key, String(ts)); } catch (e) {}
  }
  function webdavAutoRemove(key) {
    try { localStorage.removeItem(key); } catch (e) {}
  }
  function getWebdavConfig() {
    try {
      const saved = typeof GM_getValue === 'function' ? GM_getValue(WEBDAV_CONFIG_KEY, {}) : {};
      return Object.assign({ url: '', username: '', password: '', autoSync: false, lastSyncAt: 0 }, saved);
    } catch (e) {
      return { url: '', username: '', password: '', autoSync: false, lastSyncAt: 0 };
    }
  }
  function storeWebdavConfig(cfg) {
    try { if (typeof GM_setValue === 'function') GM_setValue(WEBDAV_CONFIG_KEY, cfg); } catch (e) {}
    return cfg;
  }
  function ensureWebdavConfigInPanel() {
    const cfg = getWebdavConfig();
    const $url = $('#sp_webdavUrl');
    if (!$url.length) return;
    if (!$url.val()) $url.val(cfg.url);
    if (!$('#sp_webdavUsername').val()) $('#sp_webdavUsername').val(cfg.username);
    if (!$('#sp_webdavPassword').val()) $('#sp_webdavPassword').val(cfg.password);
    $('#sp_webdavAutoSync').prop('checked', cfg.autoSync);
  }
  function saveWebdavConfigFromPanel(showToast) {
    const prev = getWebdavConfig();
    const cfg = {
      url: String($('#sp_webdavUrl').val() || '').trim(),
      username: String($('#sp_webdavUsername').val() || '').trim(),
      password: String($('#sp_webdavPassword').val() || ''),
      autoSync: !!$('#sp_webdavAutoSync').is(':checked'),
      lastSyncAt: prev.lastSyncAt || 0
    };
    storeWebdavConfig(cfg);
    if (showToast && typeof toast === 'function') toast('WebDAV 配置已保存');
    // 刚打开自动同步开关(false→true):立即同步一次,并让计时器从此刻起算
    // 防重:60s 冷却 + 内容指纹(无变化不写服务器)
    if (showToast && cfg.autoSync && !prev.autoSync && cfg.url) {
      // 开启自动同步(保存路径):立即同步一次(防重逻辑在 webdavTriggerImmediateSync 内)
      webdavTriggerImmediateSync(cfg);
    }
  }
  // 开启自动同步触发的立即同步:60s 冷却 + 内容指纹防重 + 互斥锁
  function webdavTriggerImmediateSync(cfg) {
    const now = webdavAutoNow();
    webdavAutoWrite(WEBDAV_AUTO_NEXT_KEY, now + WEBDAV_AUTO_INTERVAL_MS);
    // 冷却层:短时间连续开关不再每次触发
    const lastImmediate = webdavAutoRead(WEBDAV_AUTO_LAST_IMMEDIATE_KEY);
    if (now - lastImmediate < WEBDAV_IMMEDIATE_COOLDOWN_MS) {
      webdavAutoWrite(WEBDAV_AUTO_LAST_IMMEDIATE_KEY, now);
      if (typeof toast === 'function') toast('WebDAV:刚同步过,已跳过本次立即同步');
      return;
    }
    // 指纹层:本次要上传的内容与上次成功同步时一致则跳过
    const utils = getWebdavUtils();
    if (utils && typeof utils.buildFullExportFile === 'function') {
      const fp = webdavFingerprint(utils.buildFullExportFile(webdavFullSelection()).file.payload);
      if (fp && webdavAutoGet(WEBDAV_AUTO_LAST_FP_KEY) === fp) {
        webdavAutoWrite(WEBDAV_AUTO_LAST_IMMEDIATE_KEY, now);
        if (typeof toast === 'function') toast('WebDAV:内容无变化,跳过同步');
        return;
      }
    }
    // 走互斥锁立即同步;成功后重算指纹(下载/上传后本地内容都可能变化)
    const running = webdavAutoRead(WEBDAV_AUTO_RUNNING_KEY);
    if (now - running < 30000) {
      if (typeof toast === 'function') toast('WebDAV:有同步正在进行,请稍后重试');
      return;
    }
    webdavAutoWrite(WEBDAV_AUTO_RUNNING_KEY, now);
    webdavAutoWrite(WEBDAV_AUTO_LAST_IMMEDIATE_KEY, now);
    // 延迟 300ms,让「配置已保存/已开启」toast 先展示,再显示同步结果
    setTimeout(() => {
      webdavSyncCore(cfg, false).then(() => {
        try {
          const u2 = getWebdavUtils();
          if (u2 && typeof u2.buildFullExportFile === 'function') {
            webdavAutoWrite(WEBDAV_AUTO_LAST_FP_KEY, webdavFingerprint(u2.buildFullExportFile(webdavFullSelection()).file.payload));
          }
        } catch (e) {}
      }).finally(() => webdavAutoRemove(WEBDAV_AUTO_RUNNING_KEY));
    }, 300);
  }
  // WebDAV 请求全局限流:同步一次会连续发多个请求(读远端 / 上传前拉取 / PUT),
  // 部分 WebDAV 服务(或反向代理)对连续同连接请求会拒绝/挂起,表现为
  // 「第 1 次成功、第 2 次超时、第 3 次 HTTP 0」。这里强制最小间隔,串行化发车。
  const WEBDAV_MIN_REQUEST_GAP_MS = 400;
  let webdavLastRequestAt = 0;
  let webdavRequestChain = Promise.resolve();
  function webdavThrottle() {
    const run = webdavRequestChain.then(() => {
      const wait = webdavLastRequestAt + WEBDAV_MIN_REQUEST_GAP_MS - Date.now();
      return wait > 0 ? new Promise((r) => setTimeout(r, wait)) : Promise.resolve();
    }).then(() => { webdavLastRequestAt = Date.now(); });
    webdavRequestChain = run.catch(() => {});
    return run;
  }

  function webdavRequest(details) {
    return new Promise((resolve) => {
      if (typeof GM_xmlhttpRequest !== 'function') {
        resolve({ ok: false, status: 0, unavailable: true });
        return;
      }
      let settled = false;
      const settle = (result) => {
        if (settled) return;
        settled = true;
        resolve(result);
      };
      const req = {
        method: details.method || 'GET',
        url: details.url,
        headers: details.headers || {},
        data: details.body || null,
        responseType: details.responseType || 'text',
        timeout: details.timeout || 10000,
        onload: (resp) => settle({
          ok: resp.status >= 200 && resp.status < 300,
          status: resp.status,
          statusText: resp.statusText || '',
          responseText: resp.responseText || ''
        }),
        onerror: (err) => {
          console.error('[webdav] 请求 onerror', {
            url: details.url,
            method: details.method || 'GET',
            name: err && err.name,
            message: err && err.message ? String(err.message) : '(空)',
            rawError: err
          });
          settle({
            ok: false,
            status: 0,
            networkError: true,
            errorMessage: err && err.message ? String(err.message) : ''
          });
        },
        ontimeout: () => settle({ ok: false, status: 0, timeout: true })
      };
      try {
        // 过限流闸门后再发车(闸门是 Promise,不阻塞本次 settle 逻辑)
        webdavThrottle().then(() => { try { GM_xmlhttpRequest(req); } catch (err) {
          console.error('[webdav] GM_xmlhttpRequest 调用异常', err);
          settle({ ok: false, status: 0, callError: err && err.message ? String(err.message) : String(err) });
        } });
      } catch (err) {
        console.error('[webdav] GM_xmlhttpRequest 调用异常', err);
        settle({ ok: false, status: 0, callError: err && err.message ? String(err.message) : String(err) });
      }
      // 兜底:即使管理器的 onload/onerror/ontimeout 全部未触发,也避免 Promise 永久挂起
      setTimeout(() => settle({ ok: false, status: 0, timeout: true, hanging: true }), (details.timeout || 10000) + 3000);
    });
  }
  // 带重试的 WebDAV 请求:连续请求时偶发「超时 / HTTP 0」多为连接被复用或服务端瞬时拒绝,
  // 退避后重试一次通常即可成功。只重试网络层失败(status 0),不重试 4xx/5xx。
  async function webdavRequestRetry(details, attempts) {
    const max = Math.max(1, attempts || 2);
    let last = null;
    for (let i = 0; i < max; i += 1) {
      last = await webdavRequest(details);
      const retriable = last && last.status === 0 && (last.timeout || last.hanging || last.networkError);
      if (!retriable) return last;
      if (i < max - 1) {
        const backoff = 800 * (i + 1);
        console.warn('[webdav] 请求失败,' + backoff + 'ms 后重试', { url: details.url, method: details.method, attempt: i + 1 });
        await new Promise((r) => setTimeout(r, backoff));
      }
    }
    return last;
  }

  function buildWebdavAuthHeader(cfg) {
    const raw = cfg.username + ':' + cfg.password;
    return 'Basic ' + btoa(unescape(encodeURIComponent(raw)));
  }
  function webdavBaseUrl(cfg) {
    return String(cfg.url || '').replace(/\/+$/, '') + '/';
  }
  async function checkWebdavConnection(cfg) {
    const headers = { Authorization: buildWebdavAuthHeader(cfg) };
    const url = webdavBaseUrl(cfg);
    const isPlainHttp = /^http:\/\//i.test(url);
    if (isPlainHttp) {
      // Firefox 扩展环境默认 CSP 带 upgrade-insecure-requests,http:// 可能被升级为 https:// 导致失败
      console.warn('[webdav] HTTP 明文地址提示', { url, note: 'Firefox/Tampermonkey 可能拦截或升级明文 HTTP 请求,建议启用 HTTPS' });
    }
    // 用 GET 探测:GET 是浏览器/脚本环境都支持的基础方法,PROPFIND 在部分脚本管理器不可用
    console.log('[webdav] 检查连接请求', { method: 'GET', url });
    const r = await webdavRequest({ url, method: 'GET', headers });
    if (r.unavailable) return { ok: false, message: 'GM_xmlhttpRequest 不可用' };
    if (r.callError) return { ok: false, message: 'GM_xmlhttpRequest 异常:' + r.callError };
    if (r.hanging) return { ok: false, message: '请求无响应(脚本管理器未触发回调),请重试或检查脚本版本' };
      if (r.networkError) {
      const msg = r.errorMessage || '';
      // Tampermonkey 未授权域名时 onerror 会带 denied/connect 提示
      if (/denied|not allowed|connect/i.test(msg)) {
        return { ok: false, message: '请求被脚本管理器拦截:请在 Tampermonkey/脚本管理器中授权该 WebDAV 域名(或将其加入脚本 @connect)' };
      }
      const plainHttp = /^http:\/\//i.test(url);
      return {
        ok: false,
        message: '网络错误:无法连接到服务器'
          + (plainHttp ? '(注意:当前为 HTTP 明文地址,请确认服务器可达,若可建议改用 HTTPS)' : '')
          + (msg ? '(' + msg.slice(0, 160) + ')' : '')
      };
      }
    if (r.timeout) return { ok: false, message: '连接超时' };
    if (r.status === 401 || r.status === 403) return { ok: false, message: '认证失败(HTTP ' + r.status + '),请检查账户与密码' };
    if (r.status === 0) return { ok: false, message: '请求失败' };
    // 404/405/207 等均说明服务器可达且认证通过(目录无 index / 不支持 GET 均属正常)
    if ([200, 201, 204, 207, 301, 302, 404, 405].indexOf(r.status) >= 0) {
      // ★ 目录可达 ≠ 同步文件可读写:同步实际请求的是 base + WEBDAV_SYNC_FILE。
      //   这里追加一次文件级探测,让「连接成功」真正覆盖同步路径。
      //   预期结果:200(文件已存在)/ 404(尚未上传,属正常)/ 207 / 301 / 302 / 405
      //   失败即返回具体原因,避免「测试连接成功但同步 HTTP 0」的困惑
      const fileUrl = webdavBaseUrl(cfg) + WEBDAV_SYNC_FILE;
      // 文件探测用 HEAD:只需状态码(200 存在 / 404 不存在),不需要响应体。
      // 某些 WebDAV 对「不存在的文件」的 GET 会长时间挂起(后端尝试生成/索引),
      // 用 HEAD 能显著降低这种风险;HEAD 不被支持时再退回 GET。
      console.log('[webdav] 检查同步文件请求', { method: 'HEAD', url: fileUrl });
      let rf = await webdavRequest({ url: fileUrl, method: 'HEAD', headers, timeout: 8000 });
      if (rf.status === 405 || rf.status === 501) {
        console.log('[webdav] HEAD 不被支持,退回 GET', { status: rf.status });
        rf = await webdavRequest({ url: fileUrl, method: 'GET', headers, timeout: 8000 });
      }
      if (rf.unavailable) return { ok: false, message: '同步文件探测:GM_xmlhttpRequest 不可用' };
      if (rf.callError) return { ok: false, message: '同步文件探测异常:' + rf.callError };
      if (rf.hanging) {
        return { ok: false, message: '同步文件探测无响应:服务器对 ' + WEBDAV_SYNC_FILE + ' 的请求在 8s 内没有返回。'
          + '目录本身可达,说明地址与认证没问题;请确认该 WebDAV 服务允许读取/创建文件(部分服务对不存在的文件会长时间挂起)' };
      }
      if (rf.networkError) {
        const fmsg = rf.errorMessage || '';
        if (/denied|not allowed|connect/i.test(fmsg)) {
          return { ok: false, message: '同步文件被脚本管理器拦截:请授权该 WebDAV 域名(或加入 @connect)' + (fmsg ? '(' + fmsg.slice(0, 120) + ')' : '') };
        }
        return { ok: false, message: '同步文件网络错误:无法访问 ' + WEBDAV_SYNC_FILE + (fmsg ? '(' + fmsg.slice(0, 160) + ')' : '') };
      }
      if (rf.timeout) return { ok: false, message: '同步文件探测超时(目录可达,但文件请求超时)' };
      if (rf.status === 401 || rf.status === 403) return { ok: false, message: '同步文件认证失败(HTTP ' + rf.status + ')' };
      if (rf.status === 0) return { ok: false, message: '同步文件请求失败(HTTP 0):' + (rf.timeout ? '超时' : '网络层错误,请查看控制台 [webdav] 日志') };
      if ([200, 201, 204, 207, 301, 302, 404, 405].indexOf(rf.status) >= 0) {
        return { ok: true, message: '连接成功(目录 HTTP ' + r.status + ',同步文件 HTTP ' + rf.status + ')' };
      }
      return { ok: false, message: '同步文件异常响应 HTTP ' + rf.status + (rf.statusText ? ' ' + rf.statusText : '') };
    }
    return { ok: false, message: '异常响应 HTTP ' + r.status + (r.statusText ? ' ' + r.statusText : '') };
  }
  function setWebdavStatus(text) {
    const $el = $('#sp_webdavStatus');
    if ($el && $el.length) $el.text(text);
  }
  function webdavPanelField(name) {
    // 模板渲染时读取已保存配置,让输入框默认显示(无需先点击)
    try {
      const cfg = getWebdavConfig();
      if (name === 'url') return cfg.url || '';
      if (name === 'username') return cfg.username || '';
      if (name === 'password') return cfg.password || '';
      if (name === 'autoSync') return cfg.autoSync ? 'checked' : '';
      if (name === 'lastSync') return webdavFormatLastSync(cfg.lastSyncAt || 0);
    } catch (e) {}
    return '';
  }
  function webdavFormatLastSync(ts) {
    if (!ts) return '尚未同步';
    return '上次同步 ' + webdavFormatTime(ts);
  }
  function webdavFormatTime(ts) {
    try {
      const d = new Date(ts);
      const pad = (n) => String(n).padStart(2, '0');
      return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
    } catch (e) { return '?'; }
  }
  function webdavUpdateLastSyncLabel() {
    const $el = $('#sp_webdavLastSyncLabel');
    if ($el && $el.length) $el.text(webdavFormatLastSync((getWebdavConfig().lastSyncAt) || 0));
  }
  // 与 tag1 FIXED_KEYS 中的固定启用项对齐:这些键不参与「本地是否默认」与差异提示,避免噪声
  const WEBDAV_SETTINGS_SKIP_KEYS = new Set([
    'enableImageHideMode', 'interceptReplyForm', 'updateReplyNumbers', 'replaceRightSidebar',
    'enablePostExpandAll', 'kaomojiEnhancer', 'enableImageViewerMode', 'autoSelectReportReason',
    'enableFavoriteThreads', 'enableThreadHistory', 'enablePostHistory',
    // 分组顺序元数据:走静默合并(at 新者胜),不参与差异弹窗判定
    'groupOrderMeta', 'enableSubscriptionFeed',
    // 默认订阅号:按 updatedAt 静默合并(新者胜),不参与差异弹窗判定
    'defaultSubscriptionFeedUuid', 'defaultSubscriptionFeedUpdatedAt'
  ]);
  function sameSettingValue(a, b) {
    if (a === b) return true;
    if (a == null || b == null) return a === b;
    if (typeof a !== typeof b) return false;
    if (Array.isArray(a) || Array.isArray(b)) {
      if (!Array.isArray(a) || !Array.isArray(b)) return false;
      if (a.length !== b.length) return false;
      for (let i = 0; i < a.length; i++) {
        if (!sameSettingValue(a[i], b[i])) return false;
      }
      return true;
    }
    if (typeof a === 'object') {
      const ak = Object.keys(a).sort();
      const bk = Object.keys(b).sort();
      if (ak.length !== bk.length) return false;
      for (let i = 0; i < ak.length; i++) {
        if (ak[i] !== bk[i]) return false;
        if (!sameSettingValue(a[ak[i]], b[bk[i]])) return false;
      }
      return true;
    }
    return false;
  }
  // 设置键 → 中文短名称(用于 WebDAV 冲突弹窗等需要简明标识的场景;与 spDescriptions 的键对应)
  const SETTING_LABELS = {
    enableCookieSwitch: '饼干切换器',
    enableCookieConfirm: '发送前饼干确认',
    disableWatermark: '取消默认水印',
    enablePaginationDuplication: '添加页首页码',
    updatePreviewCookie: '预览框显示真实饼干',
    hideEmptyTitleEmail: '隐藏空标题/无名氏',
    enableExternalImagePreview: '外部图床预览',
    enableAutoCookieRefresh: '自动刷新饼干',
    enableAutoCookieRefreshToast: '自动刷新提示',
    interceptReplyFormUnvcode: 'unvcode替换模式',
    interceptReplyFormU200B: '零宽空格模式',
    interceptReplyFormAutoCompress: '自动压缩图片',
    enableSeamlessPaging: '无缝翻页',
    enableAutoSeamlessPaging: '自动无缝翻页',
    enableHDImageAndLayoutFix: '原图加载与布局修复',
    enableLinkBlank: '链接新标签页打开',
    enableAutoUrlLinkify: '网址自动转链接',
    enableQuotePreview: '优化引用弹窗',
    enableUpdateCheck: '自动检查更新',
    enableImageContextMenu: '图片右键菜单',
    enableImageHideMode: '图片显示模式',
    applyImageHideMode: '图片隐藏模式应用',
    imageViewerSeparators: '阅图模式分隔线',
    enableDraft: '草稿功能',
    enableThreadHistory: '浏览历史',
    enablePostHistory: '发言历史',
    timeDisplayMode: '时间显示方式',
    extendQuote: '拓展引用格式',
    extendQuoteAvailabilityDetection: '引用号检测',
    enablePostExpandAll: '展开/收起长串',
    kaomojiSort: '颜文字排序',
    toggleSidebar: '自动收起侧边栏',
    enableCustomDarkTheme: '深色模式',
    dockDisplayMode: '扩展坞显示模式',
    postAfterAction: '发串后行为',
    disableAutoQuote: '禁用自动引用',
    threadCookieWhitelistGroups: '只看饼干分组',
    threadCookieWhitelistDisplayMode: '只看饼干显示模式',
    poAnnotationSideDisplayMode: '批注侧栏显示模式',
    replyModeDefault: '默认回复模式',
    replyExtraDefault: '默认回复附加项',
    markedGroups: '标记饼干分组',
    blockedCookies: '屏蔽饼干分组',
    blockedKeywords: '屏蔽关键词分组',
    favoriteThreads: '常用串分组',
    subscriptionFeeds: '订阅号分组',
    defaultSubscriptionFeedUuid: '默认订阅号',
    defaultSubscriptionFeedUpdatedAt: '默认订阅号更新时间',
    groupOrderMeta: '分组排序',
    blockDisplayMode: '屏蔽显示方式',
  };
  // 取设置项中文名(未知键回退为原键名)
  function getSettingLabel(key) {
    return SETTING_LABELS[key] || key;
  }
  function webdavShowSettingsConflictDialog(diffInfo) {
    // 三选:采用远端 / 保留本地上传 / 取消本次同步(Promise 化)
    return new Promise((resolve) => {
      const backdrop = document.createElement('div');
      backdrop.style.cssText = 'position:fixed;inset:0;z-index:2147483000;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;';
      const panel = document.createElement('div');
      // 使用设置面板的 dark reader 兼容变量
      panel.style.cssText = 'background:var(--xdex-sp-panel-bg, #FFFFEE);color:var(--foreground, #333);border:1px solid var(--xdex-sp-border, #ccc);border-radius:8px;box-shadow:0 8px 24px var(--xdex-sp-shadow, rgba(0,0,0,.3));padding:16px 18px;max-width:min(460px,92vw);font-size:13px;line-height:1.6;';
      // 中文名 + 加粗醒目色(多个用顿号分隔;超过 6 个折叠为省略号)
      const labelOf = (k) => {
        const name = getSettingLabel(k);
        const changed = name !== k; // 未收录的键保持原样(不加醒目色)
        return changed
          ? `<b style="color:var(--xdex-conflict-key, #C0392B);font-weight:700;">${name}</b>`
          : `<b style="font-weight:700;">${name}</b>`;
      };
      const shown = diffInfo.diff.slice(0, 6).map(labelOf).join('、');
      const sample = shown + (diffInfo.diff.length > 6 ? '…' : '');
      panel.innerHTML = ''
        + '<div style="font-size:15px;font-weight:700;margin-bottom:8px;">检测到设置与远端不同</div>'
        + '<div style="margin-bottom:4px;">共 ' + diffInfo.diff.length + ' 项:' + sample + (diffInfo.localIsDefault ? '(本地当前为默认设置)' : '') + '</div>'
        + '<div style="color:var(--muted-foreground, #666);margin-bottom:12px;">请选择保留哪个版本的设置:</div>'
        + '<div style="display:flex;flex-direction:column;gap:8px;">'
        + '<button data-act="remote" style="padding:6px 10px;cursor:pointer;background:var(--xdex-sp-fold-bg, #F0E0D6);border:1px solid var(--xdex-sp-border, #ccc);border-radius:6px;color:var(--foreground, #333);">采用远端设置(覆盖本地)</button>'
        + '<button data-act="local" style="padding:6px 10px;cursor:pointer;background:var(--xdex-sp-fold-bg, #F0E0D6);border:1px solid var(--xdex-sp-border, #ccc);border-radius:6px;color:var(--foreground, #333);">保留本地设置(上传覆盖远端)</button>'
        + '<button data-act="cancel" style="padding:6px 10px;cursor:pointer;background:var(--xdex-sp-fold-bg, #F0E0D6);border:1px solid var(--xdex-sp-border, #ccc);border-radius:6px;color:var(--foreground, #333);">取消本次同步</button>'
        + '</div>';
      backdrop.appendChild(panel);
      document.body.appendChild(backdrop);
      const done = (act) => {
        try { document.body.removeChild(backdrop); } catch (e) {}
        document.removeEventListener('keydown', onKey, true);
        resolve(act);
      };
      const onKey = (ev) => { if (ev.key === 'Escape') done('cancel'); };
      panel.addEventListener('click', (e) => {
        const btn = e.target && e.target.closest ? e.target.closest('button[data-act]') : null;
        if (btn) { e.stopPropagation(); done(btn.getAttribute('data-act')); }
      });
      backdrop.addEventListener('click', (e) => { if (e.target === backdrop) done('cancel'); });
      document.addEventListener('keydown', onKey, true);
    });
  }
  // ── 默认订阅号跨端合并(时间戳新者胜)──
  // 规则:
  //  - 两端都从未设置过(无时间戳且无值)→ 保持空
  //  - 一端从未设置过(updatedAt=0)→ 另一端的值直接生效(若另一方明确清空也照此处理)
  //  - 两端都有时间戳 → 新者胜;清空同样带时间戳,因此“清空”可以覆盖旧的默认
  function mergeDefaultSubscriptionFeedSetting(localSettings, remoteSettings) {
    const l = localSettings || {};
    const r = remoteSettings || {};
    const lUuid = String(l.defaultSubscriptionFeedUuid || '').trim();
    const rUuid = String(r.defaultSubscriptionFeedUuid || '').trim();
    const lAt = Math.max(0, Number(l.defaultSubscriptionFeedUpdatedAt) || 0);
    const rAt = Math.max(0, Number(r.defaultSubscriptionFeedUpdatedAt) || 0);
    if (!lAt && !rAt) return { uuid: rUuid || lUuid, updatedAt: 0 };
    if (!lAt) return { uuid: rUuid, updatedAt: rAt };
    if (!rAt) return { uuid: lUuid, updatedAt: lAt };
    return rAt > lAt ? { uuid: rUuid, updatedAt: rAt } : { uuid: lUuid, updatedAt: lAt };
  }
  // 把远端默认订阅号按时间戳并入本地设置。
  // 独立于“设置差异三选”决策:默认订阅号始终静默按时间戳合并,不参与冲突弹窗。
  function applyRemoteDefaultSubscriptionFeed(remoteSettings) {
    if (!remoteSettings || typeof remoteSettings !== 'object') return false;
    if (!('defaultSubscriptionFeedUuid' in remoteSettings) && !('defaultSubscriptionFeedUpdatedAt' in remoteSettings)) return false;
    try {
      const local = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      const winner = mergeDefaultSubscriptionFeedSetting(local, remoteSettings);
      const localUuid = String(local.defaultSubscriptionFeedUuid || '').trim();
      const localAt = Math.max(0, Number(local.defaultSubscriptionFeedUpdatedAt) || 0);
      if (winner.uuid === localUuid && winner.updatedAt === localAt) return false;
      const next = Object.assign({}, local, {
        defaultSubscriptionFeedUuid: winner.uuid,
        defaultSubscriptionFeedUpdatedAt: winner.updatedAt
      });
      GM_setValue(SettingPanel.key, next);
      if (typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state) {
        SettingPanel.state.defaultSubscriptionFeedUuid = winner.uuid;
        SettingPanel.state.defaultSubscriptionFeedUpdatedAt = winner.updatedAt;
        try { if (typeof SettingPanel.syncInputs === 'function') SettingPanel.syncInputs(); } catch (e) {}
      }
      console.log('[webdav] 默认订阅号按时间戳合并', { uuid: winner.uuid, updatedAt: winner.updatedAt });
      return true;
    } catch (e) {
      console.warn('[webdav] 合并默认订阅号失败', e);
      return false;
    }
  }
  // 标记饼干自定义配色:独立于“设置差异三选”,始终按配色规则静默合并(与默认订阅号同思路)。
  // 只更新已存在分组的 color/colorAt,不在此处增删分组或改动饼干列表。
  function applyRemoteMarkedGroupColors(remoteSettings) {
    if (!remoteSettings || typeof remoteSettings !== 'object' || !Array.isArray(remoteSettings.markedGroups)) return false;
    try {
      const local = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
      const localGroups = normalizeMarkedGroups(local.markedGroups);
      const remoteGroups = normalizeMarkedGroups(remoteSettings.markedGroups);
      if (!localGroups.length || !remoteGroups.length) return false;
      let changed = false;
      const nextGroups = localGroups.map((localGroup) => {
        const remoteGroup = remoteGroups.find((g) => String((g && g.desc) || '') === localGroup.desc);
        if (!remoteGroup) return localGroup;
        const resolved = resolveMarkedGroupColorState(localGroup, remoteGroup);
        if (!resolved) return localGroup;
        if (resolved.color === localGroup.color && resolved.colorAt === localGroup.colorAt) return localGroup;
        changed = true;
        return Object.assign({}, localGroup, { color: resolved.color, colorAt: resolved.colorAt });
      });
      if (!changed) return false;
      const next = Object.assign({}, local, { markedGroups: nextGroups });
      GM_setValue(SettingPanel.key, next);
      if (typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state) {
        SettingPanel.state.markedGroups = nextGroups;
        try { if (typeof SettingPanel.syncInputs === 'function') SettingPanel.syncInputs(); } catch (e) {}
        try { if (typeof refreshFilterDisplay === 'function') refreshFilterDisplay(SettingPanel.state); } catch (e) {}
      }
      console.log('[webdav] 标记配色按规则合并', nextGroups.filter((g) => g.colorAt > 0).map((g) => ({ desc: g.desc, color: g.color, colorAt: g.colorAt })));
      return true;
    } catch (e) {
      console.warn('[webdav] 合并标记配色失败', e);
      return false;
    }
  }
  // 差异检测时忽略标记分组的纯配色差异:配色走独立的时间戳静默合并,不应触发设置冲突三选
  function sameMarkedGroupsIgnoringColors(a, b) {
    const strip = (arr) => (Array.isArray(arr) ? arr : []).map((group) => {
      if (!group || typeof group !== 'object' || Array.isArray(group)) return group;
      const copy = Object.assign({}, group);
      delete copy.color;
      delete copy.colorAt;
      return copy;
    });
    return sameSettingValue(strip(a), strip(b));
  }
  function findWebdavSettingsDiff(remoteSettings) {
    // 返回 { diff: 两端不同的设置键, localIsDefault: 本地是否仍是默认值 }
    const result = { diff: [], localIsDefault: true };
    try {
      const defaults = SettingPanel && SettingPanel.defaults ? SettingPanel.defaults : {};
      const local = Object.assign({}, defaults, GM_getValue(SettingPanel.key, {}));
      if (!remoteSettings || typeof remoteSettings !== 'object') return result;
      Object.keys(defaults).forEach((k) => {
        if (WEBDAV_SETTINGS_SKIP_KEYS.has(k)) return;
        if (!sameSettingValue(local[k], defaults[k])) result.localIsDefault = false;
        const same = k === 'markedGroups' ? sameMarkedGroupsIgnoringColors : sameSettingValue;
        if (!(k in remoteSettings) || !same(local[k], remoteSettings[k])) result.diff.push(k);
      });
      // 远端比 defaults 多的键(未来版本新增)也视为差异
      Object.keys(remoteSettings).forEach((k) => {
        if (!(k in defaults) && !WEBDAV_SETTINGS_SKIP_KEYS.has(k)) result.diff.push(k);
      });
    } catch (e) {}
    return result;
  }
  function webdavFullSelection() {
    // 同步文件不含 WebDAV 配置(用户自行选择同步源更合理);手动导入导出才迁移配置
    return { settings: true, threadHistory: true, postHistory: true, drafts: false, kaomojiStats: true, cookiePrefs: true };
  }
  function getWebdavUtils() {
    // full-export 工具定义在 SettingPanel.render 嵌套作用域,需先打开过一次设置面板(按钮本身在面板内)
    return window.__xdexWebdavUtils || null;
  }
  async function webdavUploadLocal(cfg, headers) {
    const utils = getWebdavUtils();
    if (!utils || typeof utils.buildFullExportFile !== 'function') {
      return { ok: false, status: 0, missingUtils: true };
    }
    const built = utils.buildFullExportFile(webdavFullSelection());
    let historyMerged = false; // 本次上传是否经过“基线增量”合并;裸上传需另行推进基线
    // 上传前先拉取远端(若存在):浏览历史按“基线增量”合并、发言历史按 postId/指纹归一化合并,
    // 避免全量覆盖其他端已贡献的独立记录
    let existing = null;
    try {
      existing = await webdavRequestRetry({ url: webdavBaseUrl(cfg) + WEBDAV_SYNC_FILE, method: 'GET', headers, timeout: 15000 });
    } catch (e) {}
    if (existing && existing.status >= 200 && existing.status < 300 && built.file.payload) {
      try {
        // webdavRequest 返回 GM 风格响应:正文在 responseText(无 .text() 方法)
        const existingText = String(existing.responseText || '');
        const parsed = utils.parseFullExportFile(existingText);
        if (parsed.valid && parsed.data.payload) {
          if (built.file.payload.threadHistory && utils.mergeThreadHistoryStoreWebdav && parsed.data.payload.threadHistory) {
            const baselines = utils.getWebdavHistoryBaselines ? utils.getWebdavHistoryBaselines() : {};
            const merged = utils.mergeThreadHistoryStoreWebdav(
              built.file.payload.threadHistory,
              parsed.data.payload.threadHistory,
              baselines
            );
            built.file.payload.threadHistory = merged;
            // 回写本地:upload 端也必须吸收远端贡献。否则 baseline 已推进到合并值而本地滞后,
            // 下轮 delta = max(0, 本地旧值 − 基线) 归零,本端新增计数会被静默吞掉
            setThreadHistoryStore(merged);
            if (utils.saveWebdavHistoryBaselinesFromStore) utils.saveWebdavHistoryBaselinesFromStore(merged, baselines);
            historyMerged = true;
          }
          if (built.file.payload.postHistory && utils.mergePostHistoryStore && parsed.data.payload.postHistory) {
            // local=本端待上传数据,imported=远端数据(归一化合并,保留本端 key 优先)
            const mergedPosts = utils.mergePostHistoryStore(built.file.payload.postHistory, parsed.data.payload.postHistory);
            built.file.payload.postHistory = mergedPosts;
            // 回写本地:upload 端同样应看到远端发言,否则需等到下次 download 才能补齐
            setPostHistoryStore(mergedPosts);
          }
          if (built.file.payload.myScriptSettings && utils.mergeSettingsForWebdavUpload && parsed.data.payload.myScriptSettings) {
            // 标量项以本地为准,复杂项(分组类)双向合并后再上传
            built.file.payload.myScriptSettings = utils.mergeSettingsForWebdavUpload(built.file.payload.myScriptSettings, parsed.data.payload.myScriptSettings);
          }
        } else {
          console.warn('[webdav] 上传前合并:远端文件解析失败,将原样覆盖上传', { error: parsed && parsed.error });
        }
        console.log('[webdav] 上传内容统计', {
          threadHistoryCount: Object.keys((built.file.payload.threadHistory || {}).items || {}).length,
          postHistoryCount: Object.keys((built.file.payload.postHistory || {}).items || {}).length,
          remoteExisted: !!(parsed && parsed.valid)
        });
      } catch (e) {
        console.warn('[webdav] 上传前合并历史失败,按原样上传', e);
      }
    }
    const put = await webdavRequestRetry({
      url: webdavBaseUrl(cfg) + WEBDAV_SYNC_FILE,
      method: 'PUT',
      headers,
      body: JSON.stringify(built.file),
      // 全量导出体积较大,放宽 PUT 超时(默认 10s 对慢速上行易超时)
      timeout: 30000
    });
    const putOk = put.status >= 200 && put.status < 300;
    // 裸上传(远端缺失或内容无效,未经上面的“基线增量”合并)成功后必须把基线推进到本次上传值:
    // 否则该端基线停留在缺失/旧值,下一轮双向合并会把整份本地计数当成新增再加一遍(多端倍增)
    if (putOk && !historyMerged && built.file.payload && built.file.payload.threadHistory
        && typeof utils.saveWebdavHistoryBaselinesFromStore === 'function') {
      try {
        utils.saveWebdavHistoryBaselinesFromStore(
          built.file.payload.threadHistory,
          typeof utils.getWebdavHistoryBaselines === 'function' ? utils.getWebdavHistoryBaselines() : {}
        );
      } catch (e) {
        console.warn('[webdav] 裸上传后推进基线失败', e);
      }
    }
    return { ok: putOk, status: put.status };
  }
  async function webdavSyncCore(cfg, silent) {
    const notify = (t) => { if (!silent && typeof toast === 'function') toast(t); };
    setWebdavStatus('同步中……');
    // 不再单独调用 checkWebdavConnection:它本身要发 2 个请求(目录 + 文件),
    // 而紧接着的 GET 远端已经能覆盖「可达性 + 认证 + 文件状态」,省下这 2 个请求可显著
    // 降低连续请求造成的服务端压力(此前一次同步最多 5 个请求紧挨着发)。
    const headers = { Authorization: buildWebdavAuthHeader(cfg) };
    const remoteUrl = webdavBaseUrl(cfg) + WEBDAV_SYNC_FILE;
    const remote = await webdavRequestRetry({ url: remoteUrl, method: 'GET', headers, timeout: 15000 });
    if (remote.status === 404) {
      // 远端无数据:上传本地
        const up = await webdavUploadLocal(cfg, headers);
      if (up.ok) {
        const now = Date.now();
        storeWebdavConfig(Object.assign({}, cfg, { lastSyncAt: now }));
        webdavUpdateLastSyncLabel();
        console.log('[webdav] 同步成功(远端无数据上传)', { direction: 'upload', lastSyncAt: now });
        setWebdavStatus('远端无数据,本地数据已上传');
        notify('WebDAV:本地数据已上传');
        return { ok: true, direction: 'upload' };
      }
      console.warn('[webdav] 上传失败', { direction: 'upload-first', status: up.status });
      setWebdavStatus('上传失败(HTTP ' + up.status + ')');
      notify('WebDAV 上传失败:HTTP ' + up.status);
      return { ok: false, reason: 'upload-failed' };
    }
    if (remote.status !== 200) {
      // 区分 status:0 的成因(超时/网络层错误/管理器未回调),给出可操作的提示
      let reason = '';
      if (remote.status === 0) {
        if (remote.timeout) reason = remote.hanging ? '请求无响应(脚本管理器未触发回调)' : '请求超时';
        else if (remote.networkError) {
          const m = remote.errorMessage || '';
          reason = /denied|not allowed|connect/i.test(m) ? '请求被脚本管理器拦截(请授权域名)' : ('网络错误' + (m ? ':' + m.slice(0, 120) : ''));
        }
        else if (remote.callError) reason = '调用异常:' + remote.callError;
        else reason = '网络层错误(详见控制台 [webdav] 日志)';
      }
      const detail = remote.status === 0 ? '(HTTP 0:' + reason + ')' : '(HTTP ' + remote.status + ')';
      setWebdavStatus('读取远端失败' + detail);
      notify('WebDAV 读取远端失败' + detail);
      return { ok: false, reason: 'remote-read' };
    }
    // 远端存在:按导出时间 last-write-wins
    let remoteExportedAt = 0;
    try {
      const head = JSON.parse(remote.responseText);
      remoteExportedAt = head && head.meta && head.meta.exportedAt ? Date.parse(head.meta.exportedAt) : 0;
    } catch (e) {}
    console.log('[webdav] 同步开始:双向合并模式', { remoteExportedAt: remoteExportedAt || null, lastSyncAt: cfg.lastSyncAt || 0 });
    const utils = getWebdavUtils();
    if (!utils || typeof utils.parseFullExportFile !== 'function' || typeof utils.applyFullImportPayload !== 'function') {
      setWebdavStatus('恢复功能未就绪:请先打开一次设置面板后重试');
      if (!silent) notify('WebDAV:恢复功能未就绪,请先打开设置面板');
      return { ok: false, reason: 'utils-missing' };
    }
    const parsed = utils.parseFullExportFile(remote.responseText);
    if (!parsed || !parsed.valid) {
      // 远端缺失或内容非同步文件(部分 WebDAV 实现对不存在路径返回 207/HTML):无可合并内容,
      // 退化为纯上传以重建云端;upload 内部对既有远端内容的合并守卫会自行跳过无效数据
      console.warn('[webdav] 远端无有效同步文件,转为本地上传', { status: remote.status, error: parsed && parsed.error });
      const rebuild = await webdavUploadLocal(cfg, headers);
      const rebuiltAt = Date.now();
      if (rebuild.ok) {
        storeWebdavConfig(Object.assign({}, cfg, { lastSyncAt: rebuiltAt }));
        webdavUpdateLastSyncLabel();
        setWebdavStatus('远端无有效同步文件,已上传本地数据');
        notify('WebDAV:远端无有效同步文件,已上传本地数据');
        return { ok: true, direction: 'upload-rebuild' };
      }
      setWebdavStatus('上传失败(HTTP ' + (rebuild.status == null ? '未知' : rebuild.status) + ')');
      notify('WebDAV 上传失败:HTTP ' + rebuild.status);
      return { ok: false, reason: 'upload-failed' };
    }
      // WebDAV 配置不随同步覆盖:同步上传端已排除;
      // 若用户手动导出的含 webdav 配置文件被放到远端,下载时过滤掉,避免远端反向改写本地同步源
      if (parsed.data.payload && parsed.data.payload.webdavConfig) delete parsed.data.payload.webdavConfig;
      // 默认订阅号:独立于下面的“设置差异三选”,始终按 updatedAt 静默合并(新者胜)
      // —— 即使设置整体选择“保留本地”,默认订阅号也应按时间戳更新,避免两端长期分叉
      try { applyRemoteDefaultSubscriptionFeed(parsed.data.payload && parsed.data.payload.myScriptSettings); } catch (e) {
        console.warn('[webdav] 默认订阅号合并失败', e);
      }
      // 设置差异检测:手动同步弹窗让用户选择;自动同步按「本地是否默认」决定
      // 决策仅作用于 myScriptSettings 分区,其余数据分区一律双向合并
      let settingsDecision = 'download';
      let settingsSkipped = false;
      if (parsed.data.payload && parsed.data.payload.myScriptSettings) {
        const diffInfo = findWebdavSettingsDiff(parsed.data.payload.myScriptSettings);
        if (diffInfo.diff.length > 0) {
          if (silent) {
            // 自动同步:若此前手动取消过设置冲突(挂起标记),跳过设置部分,其余数据照常合并;
            // 否则按「本地是否默认」自动决策:默认→采用远端,已个性化→保留本地上传
            if (webdavAutoGet(WEBDAV_SETTINGS_PENDING_KEY) === '1') {
              settingsDecision = 'keep-local';
            } else {
              settingsDecision = diffInfo.localIsDefault ? 'download' : 'keep-local';
            }
          } else {
            // 三选对话框:采用远端 / 保留本地上传 / 取消本次同步
            const userChoice = await webdavShowSettingsConflictDialog(diffInfo);
            if (userChoice === 'cancel') {
              webdavAutoWrite(WEBDAV_SETTINGS_PENDING_KEY, '1');
              setWebdavStatus('已取消本次同步(设置存在差异,自动同步将跳过设置直到手动处理)');
              if (!silent) notify('WebDAV:已取消本次同步');
              return { ok: false, reason: 'canceled' };
            }
            webdavAutoRemove(WEBDAV_SETTINGS_PENDING_KEY);
            settingsDecision = userChoice === 'remote' ? 'download' : 'keep-local';
          }
        } else {
          // 两端设置已一致:清除挂起标记
          webdavAutoRemove(WEBDAV_SETTINGS_PENDING_KEY);
        }
      }
      // 标记饼干配色:独立于设置三选,按“默认色让步 / 手动色比时间戳”规则静默合并。
      // 放在差异判定之后:避免刚并入的远端配色把 localIsDefault 从 true 改写成 false,
      // 从而错误地影响其它设置的自动同步方向。
      try { applyRemoteMarkedGroupColors(parsed.data.payload && parsed.data.payload.myScriptSettings); } catch (e) {
        console.warn('[webdav] 标记配色合并失败', e);
      }
      if (settingsDecision !== 'download') {
        // 保留本地设置:仅剔除设置字段,其余数据分区照常双向合并
        if (parsed.data.payload && parsed.data.payload.myScriptSettings) delete parsed.data.payload.myScriptSettings;
        settingsSkipped = true;
      }
      // ── 数据分区:远端 → 本地 合并 ──
      // 快照浏览历史存储与基线:回推失败时整体回滚,保证下轮 delta 仍可计算(否则本轮增量会被基线吞掉)
      const thStoreBeforeMerge = (typeof GM_getValue === 'function') ? GM_getValue(THREAD_HISTORY_STORAGE_KEY, null) : null;
      const thBaselineBeforeMerge = (typeof utils.getWebdavHistoryBaselines === 'function') ? (utils.getWebdavHistoryBaselines() || {}) : {};
      const kaoStoreBeforeMerge = (typeof GM_getValue === 'function') ? GM_getValue('kaomojiUsageStats', null) : null;
      const report = utils.applyFullImportPayload(parsed.data, { threadHistoryMode: 'webdav-delta' });
      // 远端设置被采用后:立即同步内存 state、回显面板,并即时应用可即时生效的设置
      if (report.settings && settingsDecision === 'download' && typeof SettingPanel !== 'undefined' && SettingPanel && SettingPanel.state) {
        try {
          SettingPanel.state = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
          if (typeof SettingPanel.syncInputs === 'function') SettingPanel.syncInputs();
          if (typeof SettingPanel.syncAuxiliaryControls === 'function') SettingPanel.syncAuxiliaryControls();
          try { if (typeof renderFavoriteThreadsMenu === 'function') renderFavoriteThreadsMenu(); } catch (e) {}
          try { if (typeof refreshFilterDisplay === 'function') refreshFilterDisplay(SettingPanel.state); } catch (e) {}
          try { if (typeof window.__xdexApplyTimeDisplayMode === 'function') window.__xdexApplyTimeDisplayMode(document); } catch (e) {}
          try { if (typeof applyImageHideMode === 'function') applyImageHideMode(SettingPanel.state.applyImageHideMode || 'default', document); } catch (e) {}
        } catch (e) {
          console.warn('[webdav] 远端设置即时应用失败', e);
        }
      }

      // ── 合并后的本地 → 云端 回推 ──
      // apply 阶段已把远端贡献并入本地、基线已对齐到合并值;此处必须【直传】而不能再走 merge:
      // 若再次按基线合并,delta = max(0, 本地 − 基线) = 0,云端会被写回旧值、本轮增量被吞。
      // 直传即「云端 ← 并集」,各端独立贡献在各自 apply 阶段累入并集,天然满足跨端加算语义。
      const builtForPush = utils.buildFullExportFile(webdavFullSelection());
      const putResp = await webdavRequest({
        url: remoteUrl,
        method: 'PUT',
        headers,
        body: JSON.stringify(builtForPush.file),
        timeout: 30000
      });
      const pushOk = putResp.status >= 200 && putResp.status < 300;
      const now = Date.now();

      if (!pushOk) {
        // 回滚浏览历史存储与基线到合并前:本地与云端重新解耦,下轮 delta = 本地 − 旧基线 仍完整可算
        try {
          if (thStoreBeforeMerge !== null) setThreadHistoryStore(thStoreBeforeMerge);
          if (kaoStoreBeforeMerge !== null) GM_setValue('kaomojiUsageStats', kaoStoreBeforeMerge);
          GM_setValue('xdex_webdav_history_baselines', thBaselineBeforeMerge || {});
        } catch (e) {
          console.error('[webdav] 回滚同步快照失败', e);
        }
        console.warn('[webdav] 双向合并完成但回推失败(已回滚本地合并结果)', { status: putResp.status, report });
        storeWebdavConfig(Object.assign({}, cfg, { lastSyncAt: cfg.lastSyncAt || 0 }));
        webdavUpdateLastSyncLabel();
        setWebdavStatus('已尝试合并远端数据,回推云端失败(HTTP ' + putResp.status + '),本轮已还原待下轮重试');
        if (!silent) notify('WebDAV:回推失败 HTTP ' + putResp.status + ',本轮改动已还原');
        return { ok: false, reason: 'push-back-failed', report };
      }
      storeWebdavConfig(Object.assign({}, cfg, { lastSyncAt: Math.max(now, remoteExportedAt || 0) }));
      webdavUpdateLastSyncLabel();

      console.log('[webdav] 同步成功(双向合并)', { remoteExportedAt: remoteExportedAt || null, pushedAt: now, report });
      const parts = [];
      if (report.settings && !settingsSkipped) parts.push('设置');
      if (report.threadHistory) parts.push('浏览历史');
      if (report.postHistory) parts.push('发言历史');
      if (report.drafts) parts.push('草稿');
      if (report.kaomojiStats) parts.push('颜文字统计');
      if (report.cookiePrefs) parts.push('饼干偏好');
      setWebdavStatus(settingsSkipped ? '双向合并完成(设置保留本地等待手动处理)' : '双向合并完成');
      notify('WebDAV:双向合并完成(' + (parts.join('、') || '无变化') + ');可即时生效的设置已应用');
      return { ok: true, direction: 'bidirectional' };
  }
  async function webdavSyncNow() {
    saveWebdavConfigFromPanel(false);
    const cfg = getWebdavConfig();
    if (!cfg.url) {
      if (typeof toast === 'function') toast('请先填写 WebDAV 链接');
      return;
    }
    // 与自动同步共用互斥锁:避免手动/自动或多标签并发双写(30s 窗口)
    const now = webdavAutoNow();
    const running = webdavAutoRead(WEBDAV_AUTO_RUNNING_KEY);
    if (now - running < 30000) {
      console.warn('[webdav] 手动同步跳过:其他同步正在进行');
      if (typeof toast === 'function') toast('WebDAV:有同步正在进行,请稍后重试');
      return;
    }
    webdavAutoWrite(WEBDAV_AUTO_RUNNING_KEY, now);
    // 手动同步后重置自动同步计时器:避免紧随的空转
    webdavAutoWrite(WEBDAV_AUTO_NEXT_KEY, now + WEBDAV_AUTO_INTERVAL_MS);
    try {
      await webdavSyncCore(cfg, false);
    } finally {
      webdavAutoRemove(WEBDAV_AUTO_RUNNING_KEY);
    }
  }
  function webdavAutoSyncIfEnabled() {
    const cfg = getWebdavConfig();
    if (!cfg.url || !cfg.autoSync) return;
    const now = webdavAutoNow();
    const next = webdavAutoRead(WEBDAV_AUTO_NEXT_KEY);
    let scheduleHandle = 0;
    const trySync = () => {
      // 到点执行时重新取当前时间(排程等待期间可能已过去很久)
      const tryNow = webdavAutoNow();
      // 复检开关:关闭自动同步后,本页已排程的定时器不再执行
      if (!getWebdavConfig().autoSync) {
        console.log('[webdav] 自动同步跳过:开关已关闭');
        return;
      }
      // 抢锁:避免多标签页同时同步;30s 窗口内其他页面跳过本次
      const running = webdavAutoRead(WEBDAV_AUTO_RUNNING_KEY);
      if (tryNow - running < 30000) {
        // 同一端的其他页面正在同步:页面间共享同一份 GM 存储,
        // 持锁页面同步的就是全量共享数据,本页无需重复同步(更不应额外排重试);
        // next 已被其推进,本页随共享计划等待下一轮即可
        console.log('[webdav] 自动同步跳过:其他页面正在同步(同端数据共享,无需重复同步)');
        scheduleNext();
        return;
      }
      // 抢到锁
      webdavAutoWrite(WEBDAV_AUTO_RUNNING_KEY, tryNow);
      // 无论成败都推进下次时间,避免失败后每个页面都重试
      webdavAutoWrite(WEBDAV_AUTO_NEXT_KEY, tryNow + WEBDAV_AUTO_INTERVAL_MS);
      console.log('[webdav] 自动同步开始', { at: new Date(tryNow).toLocaleString(), intervalMs: WEBDAV_AUTO_INTERVAL_MS });
      const done = () => {
        webdavAutoRemove(WEBDAV_AUTO_RUNNING_KEY);
        // 同步结束(无论成败)续排下一次:页面存活期间不再依赖重载
        scheduleNext();
      };
      try {
        webdavSyncCore(cfg, true).then(done, done);
      } catch (e) {
        done();
        console.warn('[webdav] auto sync failed', e);
      }
    };
    // 循环排程:每次按共享 next 排一次;到点执行后由 trySync 末尾再次调用本函数续排
    const scheduleNext = () => {
      clearTimeout(scheduleHandle);
      const cfg2 = getWebdavConfig();
      if (!cfg2.url || !cfg2.autoSync) return;
      const nextMs = Number(webdavAutoRead(WEBDAV_AUTO_NEXT_KEY)) || 0;
      const now2 = webdavAutoNow();
      const delay = nextMs ? Math.max(1000, Math.min(nextMs - now2, WEBDAV_AUTO_INTERVAL_MS)) : WEBDAV_AUTO_INTERVAL_MS;
      console.log('[webdav] 自动同步已排程', { at: new Date(now2 + delay).toLocaleString(), delayMs: delay });
      scheduleHandle = setTimeout(trySync, delay);
    };
    if (!next || now >= next) {
      // 首次或已到点(含上次页面关闭未同步):立即补一次(trySync 完成后会续排)
      console.log('[webdav] 自动同步到点,立即执行', { next: next ? new Date(next).toLocaleString() : '首次' });
      trySync();
      return;
    }
    // 未到点:排程等待;执行后由 trySync 续排,页面存活也能持续自动同步
    scheduleNext();
  }
  function bindWebdavPanelEvents() {
    // 原生 DOM 委托(capture 阶段),不依赖 jQuery / 面板重建时机 / 脚本管理器注入顺序
    try {
      // 划词抑制:面板内 mousedown 后位移超过 5px 视为拖选,阻止 click 默认行为与面板外误关
      let webdavDragStart = null;
      document.addEventListener('mousedown', function onWebdavDragStart(e) {
        if (e.button !== 0) { webdavDragStart = null; return; }
        const t = e.target;
        webdavDragStart = (t && t.closest && t.closest('#sp_panel, #sp_cover'))
          ? { x: e.clientX, y: e.clientY }
          : null;
      }, true);
      document.addEventListener('click', function onWebdavDragSuppress(e) {
        if (!webdavDragStart) return;
        const dx = e.clientX - webdavDragStart.x;
        const dy = e.clientY - webdavDragStart.y;
        if (Math.sqrt(dx * dx + dy * dy) > 5) {
          e.preventDefault();
          e.stopImmediatePropagation();
        }
        webdavDragStart = null;
      }, true);
      // 自动同步开关:变化即保存并提示;开启时立即同步一次(无需再点保存)
      document.addEventListener('change', function onWebdavAutoSyncChange(e) {
        const t = e && e.target;
        if (!t || t.id !== 'sp_webdavAutoSync') return;
        console.log('[webdav] 自动同步开关变化', { checked: t.checked });
        saveWebdavConfigFromPanel(false);
        const cfg = getWebdavConfig();
        if (t.checked) {
          if (typeof toast === 'function') toast('已开启自动同步');
          if (cfg.url) webdavTriggerImmediateSync(cfg);
        } else {
          if (typeof toast === 'function') toast('已关闭自动同步');
        }
      }, true);
      document.addEventListener('click', function onWebdavDelegateClick(e) {
        const t = e && e.target;
        if (!t || typeof t.closest !== 'function') return;
        const btn = t.closest('#btn_webdavCheck, #btn_webdavSync, #btn_sp_webdavSave, #btn_webdavTogglePassword');
        if (!btn || !btn.id) return;
        console.log('[webdav] 点击', { id: btn.id, at: Date.now() });
        if (btn.id === 'btn_sp_webdavSave') {
          e.preventDefault();
          e.stopPropagation();
          try {
            ensureWebdavConfigInPanel();
            saveWebdavConfigFromPanel(true);
          } catch (err) {
            console.error('[webdav] 保存处理异常', err);
            setWebdavStatus('保存异常:' + (err && err.message ? err.message : err));
          }
          return;
        }
        if (btn.id === 'btn_webdavCheck') {
          e.preventDefault();
          e.stopPropagation();
          try {
            ensureWebdavConfigInPanel();
            saveWebdavConfigFromPanel(false);
            const cfg = getWebdavConfig();
            if (!cfg.url) {
              console.warn('[webdav] 检查连接:未填写链接');
              setWebdavStatus('请先填写 WebDAV 链接');
              return;
            }
            console.log('[webdav] 检查连接开始', { url: cfg.url, username: cfg.username ? '已填' : '未填', password: cfg.password ? '已填' : '未填' });
            setWebdavStatus('正在检查连接……');
            checkWebdavConnection(cfg).then((conn) => {
              console.log('[webdav] 检查连接结果', conn);
              setWebdavStatus(conn.message);
              if (typeof toast === 'function') toast(conn.ok ? 'WebDAV ' + conn.message : 'WebDAV 连接失败:' + conn.message);
            }).catch((err) => {
              console.error('[webdav] 检查连接异常', err);
              setWebdavStatus('检查连接异常:' + (err && err.message ? err.message : err));
              if (typeof toast === 'function') toast('WebDAV 检查连接异常');
            });
          } catch (err) {
            console.error('[webdav] 检查连接处理异常', err);
            setWebdavStatus('检查连接异常:' + (err && err.message ? err.message : err));
          }
          return;
        }
        if (btn.id === 'btn_webdavTogglePassword') {
          e.preventDefault();
          e.stopPropagation();
          const input = document.getElementById('sp_webdavPassword');
          if (input) {
            const showing = input.type === 'text';
            input.type = showing ? 'password' : 'text';
            // 图标切换:密码显示 → 闭眼(点击后隐藏);密码隐藏 → 睁眼(点击后显示)
            btn.innerHTML = showing ? XDEX_ICON_EYE : XDEX_ICON_EYE_OFF;
            btn.title = showing ? '显示密码' : '隐藏密码';
            console.log('[webdav] 密码可见性切换', { showing: !showing });
          }
          return;
        }
        if (btn.id === 'btn_webdavSync') {
          e.preventDefault();
          e.stopPropagation();
          btn.classList.add('xdex-icon-loading');
          webdavSyncNow().catch((err) => {
            console.error('[webdav] 手动同步异常', err);
            setWebdavStatus('同步异常:' + (err && err.message ? err.message : err));
          }).finally(() => { btn.classList.remove('xdex-icon-loading'); });
        }
      }, true);
      // 面板重建后回填已保存配置:聚焦输入框时空值则回填
      document.addEventListener('focusin', function onWebdavFocusin(e) {
        const t = e && e.target;
        if (!t || !t.id) return;
        if (t.id === 'sp_webdavUrl' || t.id === 'sp_webdavUsername' || t.id === 'sp_webdavPassword') {
          try { ensureWebdavConfigInPanel(); } catch (err) { console.warn('[webdav] 回填异常', err); }
        }
      }, true);
      // 切回标签页时刷新灰字:其他页面完成同步后,此页面打开状态也能同步显示
      window.addEventListener('focus', function onWebdavWindowFocus() {
        try { webdavUpdateLastSyncLabel(); } catch (e) {}
      });
      console.log('[webdav] 面板事件绑定完成(原生委托)');
      return true;
    } catch (e) {
      console.error('[webdav] 绑定失败', e);
      return false;
    }
  }
  // 立即绑定一次即可:原生 document 监听不依赖 jQuery 就绪
  bindWebdavPanelEvents();
  // 自动同步:页面 load 后延迟执行,避开启动编排与发言历史回查高峰期
  window.addEventListener('load', () => {
    setTimeout(() => { webdavAutoSyncIfEnabled(); }, 4000);
  });

  /* --------------------------------------------------
   * tag 30. 测试板块增强:侧边栏入口 + 主串列表恢复
   * -------------------------------------------------- */
  // 测试版(fid=122)不在站点公开的侧边栏列表里(与值班室同组但被隐藏),补一个入口。
  // 链接与样式与同组其他链接(值班室、三百人委员会等)完全一致。
  // ★ 当前页/串来自测试板块时,同时展开「管理」分组(服务端不知道该板块属于侧边栏哪个分组,
  //   不会加 uk-open;值班室等默认板块则会展开——这里补齐一致体验)。
  // 识别判据(覆盖版块页与串内页):
  //   1) URL 路径以 /f/测试 开头(版块页;复用 PageType 统一解析)
  //   2) 发串表单隐藏域 input[name=fid][value=122](版块页与串内页均有表单)
  //   3) 面包屑中存在 /f/%E6%B5%8B%E8%AF%95 链接(版块页;串内页面包屑不含板块)
  //   4) forum-info 脚本的 forum.id === '122'(仅版块页)
  //   5) 串内页兜底:当前串号 ∈ 测试版串号集合(showf API 拉取,1h 缓存)
  //      —— 串内页 URL/表单/面包屑/forum-info 均不含板块信息,只能靠串号归属判定
  function isTestBoardContext() {
    try {
      // 仅 /f/测试(值班室虽同属特殊板块,但不是测试上下文)
      if (/^\/f\/%E6%B5%8B%E8%AF%95(?:\/|$)/i.test(location.pathname)
        || /^\/f\/测试(?:\/|$)/.test(decodeURIComponent(location.pathname))) return true;
      if (document.querySelector('#h-post-form input[name="fid"][value="122"], form input[name="fid"][value="122"]')) return true;
      // ★ 串内页主判据:面包屑含板块链接(服务端会输出当前串所属板块)。
      //   href 可能是编码态(/f/%E6%B5%8B%E8%AF%95)或明文态(/f/测试),逐链接解码后比对
      const bc = document.querySelector('.uk-breadcrumb');
      if (bc) {
        const links = bc.querySelectorAll('a[href^="/f/"]');
        for (const a of links) {
          const href = a.getAttribute('href') || '';
          let decoded = href;
          try { decoded = decodeURIComponent(href); } catch (e) {}
          if (/^\/f\/测试(?:\/|$)/.test(decoded)) return true;
        }
      }
      const info = document.getElementById('forum-info');
      if (info && /"id"\s*:\s*"122"/.test(info.textContent)) return true;
    } catch (e) {}
    return false;
  }
  // 展开「管理」分组(与站点服务端对默认板块的 uk-open 行为对齐)
  // ⚠️ UIkit 竞态:站点内联脚本 ready 时会初始化 nav 组件并按服务端初始状态重设高度——
  // 我们在 early pass 加的 uk-open/height 会被它覆盖(实测版块页 uk-open 丢失、串内页 height 归 0)。
  // 因此展开分两段:立即设一次;ready 后延迟(500ms/1200ms)再重申,并使用具体像素高度
  // (原生展开态是 height:130px 这类具体值,UIkit 对 auto 支持不佳)。
  function expandAdminGroupIfTestBoard(alreadyMatched) {
    if (!alreadyMatched && !isTestBoardContext()) return false;
    const menu = document.getElementById('h-menu-content') || document.querySelector('.h-menu-content');
    if (!menu) return false;
    const groupLi = Array.from(menu.children).find((li) => {
      if (!li || !li.querySelector) return false;
      const header = li.querySelector(':scope > .h-nav-parent-header, :scope > a.h-nav-parent-header');
      return header && (header.textContent || '').trim() === '管理';
    });
    if (!groupLi) return false;
    const applyExpand = () => {
      groupLi.classList.add('uk-open');
      groupLi.setAttribute('aria-expanded', 'true');
      const wrap = groupLi.querySelector(':scope > div');
      if (wrap) {
        wrap.classList.remove('uk-hidden');
        const sub = wrap.querySelector('ul.uk-nav-sub');
        // 使用具体像素高度(对齐原生展开态 height:130px 的形态),随内容自适应
        const h = sub && sub.scrollHeight ? sub.scrollHeight + 'px' : 'auto';
        wrap.style.height = h;
      }
      // 链接样式对齐原生当前版块:ultimate-bold-correct + li.h-active
      // 判定用「测试板块上下文」整体(版块页与串内页均算——串内页面包屑指向测试板块,
      // 原生对值班室串内页同样高亮其板块链接)
      const link = groupLi.querySelector('a[data-xdex-test-board-link]');
      if (link) {
        const active = isTestBoardContext();
        link.classList.toggle('ultimate-bold-correct', active);
        if (active && link.parentElement) link.parentElement.classList.add('h-active');
      }
    };
    applyExpand();
    // 重申:覆盖 UIkit 组件初始化的覆盖(ready 后 500ms / 1200ms 各一次)
    const reassert = () => { try { applyExpand(); } catch (e) {} };
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => { setTimeout(reassert, 500); setTimeout(reassert, 1200); }, { once: true });
    } else {
      setTimeout(reassert, 500);
      setTimeout(reassert, 1200);
    }
    return true;
  }
  function ensureTestBoardSidebarLink() {
    const menu = document.getElementById('h-menu-content') || document.querySelector('.h-menu-content');
    if (!menu) return false;
    const contextMatched = isTestBoardContext();
    // 幂等:已存在则跳过(同时保证展开状态被应用)
    if (menu.querySelector('a[data-xdex-test-board-link]')) { expandAdminGroupIfTestBoard(contextMatched); return true; }
    // 定位「管理」分组:<li class="uk-parent"> > a.h-nav-parent-header 文本为「管理」
    const groupLi = Array.from(menu.children).find((li) => {
      if (!li || !li.querySelector) return false;
      const header = li.querySelector(':scope > .h-nav-parent-header, :scope > a.h-nav-parent-header');
      return header && (header.textContent || '').trim() === '管理';
    });
    if (!groupLi) return false;
    const sub = groupLi.querySelector(':scope > ul.uk-nav-sub')
      || groupLi.querySelector(':scope > div > ul.uk-nav-sub')
      || groupLi.querySelector('ul.uk-nav-sub');
    if (!sub) return false;
    // 以「三百人委员会」为锚点插到其后;找不到锚点则追加到子列表末尾
    const anchor = Array.from(sub.children).find((li) => {
      const a = li.querySelector && li.querySelector(':scope > a');
      return a && (a.textContent || '').trim() === '三百人委员会';
    });
    const li = document.createElement('li');
    const link = document.createElement('a');
    link.href = 'https://www.nmbxd1.com/f/%E6%B5%8B%E8%AF%95';
    link.textContent = '测试';
    link.setAttribute('data-xdex-test-board-link', '1');
    li.appendChild(link);
    if (anchor && anchor.nextSibling) sub.insertBefore(li, anchor.nextSibling);
    else sub.appendChild(li);
    expandAdminGroupIfTestBoard(contextMatched);
    return true;
  }
  // 侧边栏可能晚于脚本渲染(站点异步构建),装观察器兜底补挂
  function installTestBoardSidebarLinkObserver() {
    ensureTestBoardSidebarLink();
    if (installTestBoardSidebarLinkObserver.__installed) return;
    installTestBoardSidebarLinkObserver.__installed = true;
    const observer = new MutationObserver(() => {
      ensureTestBoardSidebarLink();
    });
    observer.observe(document.body, { childList: true, subtree: true });
  }
  // ★ 测试版主串列表守护(v2):
  // 站点内联脚本对 /f/测试 的处理是「无条件 remove() 主串列表与分页,再请求 /Admin/Api/isManager,
  // 仅管理员放回」。但服务端对已登录用户正常下发了完整列表(含主串),移除只发生在客户端。
  //
  // v2 方案(取代 v1 的「事后拉取放回」):
  // 文档起始即装 MutationObserver 守护 document.documentElement,监听 removedNodes——
  // .h-threads-list / .h-pagination 一旦被移除,立刻在原父容器的原位置放回**同一个节点**。
  // 优点:
  //   1. 移除与放回发生在同一帧内,肉眼无闪变;
  //   2. 节点是服务端原始 DOM,早期增强 pass(回复编号/PO标记/页码扩充等)正常作用;
  //   3. 原始分页结构完好,无缝翻页可用;零额外网络请求;
  //   4. 站点管理员分支的 container.append(threadList) 是移动语义(jQuery append),
  //      节点已在容器内时为先 remove 后 append,位置可能挪到容器末尾,无实质影响。
  function installTestBoardListGuard() {
    if (!PageType.isSpecialBoard()) return;
    if (installTestBoardListGuard.__installed) return;
    installTestBoardListGuard.__installed = true;
    const GUARD_SEL = '.h-threads-list, .h-pagination';
    // 只守护主内容区(#h-content)内的列表与分页:
    // 站点脚本 $(".h-pagination") 会把侧边栏/管理分组里的分页也一并移除——那是站点自身行为,
    // 管理员放回分支也只处理主内容区,我们不越界代管,避免节点被放回错误容器。
    const inMainContent = (node) => !!(node.closest && node.closest('#h-content'));
    // ★ 结构化锚定(站点真实结构,见 /f/测试 原始 HTML):
    //   #h-post-form(含 form[action=doPostThread])→ <hr>(hr①)→ .h-threads-list → .h-pagination
    //   → <script id="forum-info">(其前有 <hr>,hr②)→ #h-footer
    // 列表应放回 hr① 之后;分页放回列表之后。不再依赖通用位置表(同批次多个 removal 时失效)。
    const findListAnchor = () => {
      const container = document.querySelector('.uk-container');
      if (!container) return null;
      const form = container.querySelector('#h-post-form form[action="/Home/Forum/doPostThread.html"]')
        || container.querySelector('#h-post-form');
      if (!form) return null;
      // hr① = 表单容器之后的第一个 <hr>
      let node = form.parentElement === container ? form : container.querySelector('#h-post-form');
      while (node && node.nextElementSibling) {
        node = node.nextElementSibling;
        if (node.tagName === 'HR') return { hr: node, container };
      }
      return { hr: null, container }; // 无 hr①:插在表单容器之后
    };
    const placeList = (list) => {
      const anchor = findListAnchor();
      if (!anchor) return false;
      try {
        if (anchor.hr && anchor.hr.parentElement === anchor.container) {
          anchor.container.insertBefore(list, anchor.hr.nextSibling);
        } else {
          anchor.container.insertBefore(list, anchor.hr || null);
        }
        // 分页紧跟列表之后(若分页节点游离,一并归位)
        const navi = document.querySelector('.h-pagination');
        if (navi && navi.parentElement !== list.parentElement) {
          list.parentElement.insertBefore(navi, list.nextSibling);
        }
        return true;
      } catch (e) { return false; }
    };
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        if (!mutation.removedNodes || !mutation.removedNodes.length) continue;
        for (const node of mutation.removedNodes) {
          if (node.nodeType !== 1) continue;
          if (!node.matches || !node.matches(GUARD_SEL)) continue;
          // ★ 只守护「站点原生分页」:enablePaginationDuplication 克隆的页首页码(titlePag)
          //   不在守护范围——它的插入/移除由页码功能自己管理,守护器若放回会与其去重逻辑死循环
          if (node.dataset && node.dataset.xdexTitleClone === '1') continue;
          // 无缝翻页接管标记:replaceWith 更新页码前的有意移除,守护器放行
          if (node.dataset && node.dataset.xdexSeamlessReplaced === '1') continue;
          // 列表:结构化放回(hr① 之后)
          if (node.classList.contains('h-threads-list')) {
            if (placeList(node)) console.info('[X岛-EX][测试版列表] 已守护放回(hr① 下方)');
            continue;
          }
          // 分页:结构化放回列表之后(页底)
          if (node.classList.contains('h-pagination')) {
            const list = document.querySelector('#h-content .h-threads-list');
            const container = document.querySelector('.uk-container');
            try {
              if (list && list.parentElement === container) {
                container.insertBefore(node, list.nextSibling);
                console.info('[X岛-EX][测试版分页] 已守护放回页底 (items=' + node.querySelectorAll('li').length + ')');
              } else if (container) {
                const info = container.querySelector('script#forum-info');
                container.insertBefore(node, info || document.getElementById('h-footer') || null);
                console.info('[X岛-EX][测试版分页] 已守护放回(列表未就位,暂放 forum-info 前)');
              } else {
                console.info('[X岛-EX][测试版分页] 未找到 .uk-container,跳过');
              }
            } catch (e) {
              console.warn('[X岛-EX][测试版分页] 放回失败', e);
            }
          }
        }
      }
    });
    const start = () => {
      if (!document.documentElement) { setTimeout(start, 25); return; }
      observer.observe(document.documentElement, { childList: true, subtree: true });
    };
    start();
  }
  // ★ 兜底:守护器万一失手(极端竞态/父容器销毁),ready+400ms 后检查一次并网络拉取放回。
  // 正常情况下守护器已放回节点,这里直接返回;保留 v1 逻辑作为最后防线。
  function restoreTestBoardThreadList() {
    if (!PageType.isSpecialBoard()) return;
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => setTimeout(restoreTestBoardThreadList, 400), { once: true });
      return;
    }
    setTimeout(restoreTestBoardThreadList.__attempt, 400);
  }
  restoreTestBoardThreadList.__attempt = function __attempt() {
    if (!PageType.isSpecialBoard()) return;
    if (document.querySelector('.h-threads-list')) return; // 已有列表(管理员或已守护恢复)
    if (restoreTestBoardThreadList.__running) return;
    restoreTestBoardThreadList.__running = true;
    const container = document.querySelector('.uk-container');
    if (!container) { restoreTestBoardThreadList.__running = false; return; }
    fetch(location.href, { credentials: 'same-origin' })
      .then((res) => (res.ok ? res.text() : Promise.reject(new Error('HTTP ' + res.status))))
      .then((html) => {
        const doc = new DOMParser().parseFromString(html, 'text/html');
        const list = doc.querySelector('.h-threads-list');
        if (!list) return; // 服务端也没给(未登录等),保持站点原状
        if (document.querySelector('.h-threads-list')) return; // 竞态:站点脚本已放回
        const navi = doc.querySelector('.h-pagination');
        container.appendChild(list);
        if (navi) container.appendChild(navi);
        console.info('[X岛-EX] 测试版主串列表已通过兜底拉取恢复');
      })
      .catch((e) => console.warn('[X岛-EX] 测试版列表恢复失败', e))
      .finally(() => { restoreTestBoardThreadList.__running = false; });
  };

  /* --------------------------------------------------
   * tag 31. 串内容导出(扩展后台任务)
   * -------------------------------------------------- */
  // 页面内提供:sp_btn 下方的「导」按钮 + 提交弹窗 + 缩略进度条。
  // 任务由扩展 Service Worker 在后台执行(页面可关闭),进度经 chrome.storage 同步。
  // 非扩展环境(Tampermonkey / QwQNT)暂不提供实现(分支留空)。
  const XDEX_EXPORT_TASKS_KEY = 'xdex-export-tasks';
  const XDEX_EXPORT_UI_ID = 'xdex-export-ui';

  // 扩展环境:内容脚本里可直接用 chrome.runtime(与主脚本同处 ISOLATED world)
  function xdexExportExtensionAvailable() {
    try {
      return !!(typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id && chrome.runtime.sendMessage);
    } catch (e) { return false; }
  }

  // 脚本猫桥接:主脚本不声明 @storageName,仅经 DOM 事件与独立的桥接脚本通信。
  // 为什么不用 window 属性握手:脚本猫给每个脚本独立的沙箱 window(沙箱属性写入不会
  // 泄漏到 unsafeWindow),主脚本读不到桥接写在自己 window 上的对象。
  // DOM 是共享的,因此以 documentElement 上的标记作为存在性信号,
  // 请求/响应一律走 DOM 事件(不需要任何跨沙箱的函数引用)。
  function xdexExportBridge() {
    try {
      const root = document.documentElement;
      const mark = root && root.dataset ? root.dataset.xdexExportBridge : '';
      if (mark) return { available: true, version: mark };
    } catch (e) { /* 忽略 */ }
    try {
      const w = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : null;
      const b = w ? w.__xdexExportBridge : null;
      if (b && b.available) return b;
    } catch (e) { /* 忽略 */ }
    return null;
  }

  function xdexExportRuntimeAvailable() {
    return xdexExportExtensionAvailable() || !!xdexExportBridge();
  }

  function xdexExportExtensionSend(message) {
    return new Promise((resolve, reject) => {
      try {
        chrome.runtime.sendMessage(message, (response) => {
          const err = chrome.runtime.lastError;
          if (err) { reject(new Error(err.message)); return; }
          if (!response) { reject(new Error('扩展后台无响应')); return; }
          if (response.ok === false) { reject(new Error(response.error || '操作失败')); return; }
          resolve(response.data);
        });
      } catch (e) { reject(e); }
    });
  }

  // 经桥接脚本转发:请求用 DOM 事件送出去,响应也用 DOM 事件送回来,
  // 桥接负责与后台脚本交换共享存储。requestId 用于把异步响应配回本次调用。
  // timeoutMs:快速请求 30s;submit 只等「受理回执」(后台接到即回,抓取在其后台继续),
  // 因此给 60s 兜底即可 —— 不像以前那样不设超时,避免后台未运行时永久卡在「正在提交…」。
  // 进度由 list 轮询呈现(按需启停,无任务时不轮询)。
  function xdexExportBridgeSend(bridge, message, timeoutMs) {
    return new Promise((resolve, reject) => {
      const requestId = 'xdex-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
      let settled = false;
      const finish = (fn, arg) => {
        if (settled) return;
        settled = true;
        clearTimeout(timer);
        document.removeEventListener('xdex:export-result', onResult);
        fn(arg);
      };
      const onResult = (ev) => {
        const d = (ev && ev.detail) || {};
        if (d.requestId !== requestId) return;
        if (d.ok === false) finish(reject, new Error(d.error || '导出操作失败'));
        else finish(resolve, d.data);
      };
      const timer = timeoutMs > 0
        ? setTimeout(() => finish(reject, new Error(
            '后台脚本无响应(超时)——请确认「X岛-EX · 脚本猫后台导出」已在脚本猫里【运行/启用】')), timeoutMs)
        : null;
      document.addEventListener('xdex:export-result', onResult);
      try {
        // 直接派发 DOM 事件(DOM 跨沙箱共享);若桥接对象恰好可用则优先用它的方法
        if (bridge && typeof bridge.request === 'function') {
          bridge.request(message.type, message.details || {}, requestId);
        } else {
          document.dispatchEvent(new CustomEvent('xdex:export-request', {
            detail: { type: String(message.type || ''), details: message.details || {}, requestId }
          }));
        }
      } catch (e) { finish(reject, e); }
    });
  }

  // HTML → 纯文本(用于从主串正文里取标题)。
  // 采用「去标签 + 折叠空白」这一最小规则,以便与扩展端/脚本猫后台的实现逐字一致。
  function xdexExportHtmlToPlain(html) {
    return String(html == null ? '' : html).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
  }

  // 标题取用规则(与后台落盘命名用的是同一套):
  //   有标题且不是「无标题」→ 用原标题;
  //   否则取主串正文的「第一句话」—— 到第一个句末标点(含)为止,没有则整段;最后统一截到 40 字。
  function xdexExportPickTitle(title, contentHtml) {
    const t = String(title || '').trim();
    if (t && t !== '无标题') return t;
    const text = xdexExportHtmlToPlain(contentHtml);
    if (!text) return '';
    const m = text.match(/^[^。!?!?]{1,60}[。!?!?]/);
    return (m ? m[0] : text).slice(0, 40).trim();
  }

  // 检索限流:批量检索时按顺序、按最小间隔发请求,避免短时间对站点发太多请求
  const XDEX_EXPORT_PROBE_MIN_INTERVAL_MS = 1000;
  let xdexExportProbeLastAt = 0;
  let xdexExportProbeChain = Promise.resolve();
  function xdexExportProbeThrottle() {
    const run = xdexExportProbeChain
      .then(() => {
        const wait = xdexExportProbeLastAt + XDEX_EXPORT_PROBE_MIN_INTERVAL_MS - Date.now();
        return wait > 0 ? new Promise((r) => setTimeout(r, wait)) : Promise.resolve();
      })
      .then(() => { xdexExportProbeLastAt = Date.now(); });
    xdexExportProbeChain = run.catch(() => {});
    return run;
  }

  // 本地探测:脚本猫环境下直接用主脚本自己的 GM_xmlhttpRequest 取串信息,
  // 无需后台脚本参与 —— 这样「检索」在后台未运行/未启用时依然可用。
  function xdexExportLocalProbe(details) {
    const tid = String((details && details.tid) || '').trim();
    if (!/^\d{4,9}$/.test(tid)) return Promise.reject(new Error('串号格式不正确'));
    const headers = (typeof getPostHistoryApiCookieHeaders === 'function') ? getPostHistoryApiCookieHeaders() : null;
    const url = `https://api.nmb.best/api/thread?id=${encodeURIComponent(tid)}&page=1`;
    return xdexExportProbeThrottle().then(() => gmRequest(url, 'text', headers)).then((resp) => {
      const raw = resp && (resp.response !== undefined ? resp.response : resp.responseText);
      let data = raw;
      if (typeof raw === 'string') {
        const text = raw.trim();
        // 站点在串不存在时返回 JSON 字符串字面量或裸文本,都要解出可读文案
        if (text.startsWith('"')) {
          try { throw new Error(JSON.parse(text)); } catch (e) { if (e instanceof Error) throw e; }
        }
        if (!text.startsWith('{') && !text.startsWith('[')) {
          throw new Error(text || '接口返回了无法解析的内容');
        }
        try { data = JSON.parse(text); } catch (e) { throw new Error('接口返回的 JSON 无法解析'); }
      }
      if (!data || data.success === false) {
        throw new Error((data && data.error) || '接口返回错误');
      }
      const totalReplies = Number(data.ReplyCount != null ? data.ReplyCount : data.reply_count) || 0;
      return {
        tid,
        // 检索展示与后台落盘命名使用同一套标题规则:无标题时取主串第一句话
        title: xdexExportPickTitle(data.title, data.content),
        rawTitle: String(data.title || '').trim(),
        fid: String(data.fid || '').trim(),
        totalPages: Math.max(1, Math.ceil(totalReplies / 19)),
        totalReplies
      };
    });
  }

  // 快速请求(probe/list/cancel)超时短;submit 只等受理回执,给足 offscreen 冷启动余量
  const XDEX_EXPORT_BRIDGE_TIMEOUT_MS = 30000;
  const XDEX_EXPORT_BRIDGE_ACK_TIMEOUT_MS = 60000;

  function xdexExportSend(message) {
    if (xdexExportExtensionAvailable()) return xdexExportExtensionSend(message);
    const bridge = xdexExportBridge();
    if (bridge) {
      // 检索不依赖后台脚本:主脚本自己就能取串信息(后台未启用时也能用)
      if (message && message.type === 'xdex:export:probe') {
        return xdexExportLocalProbe(message.details || {});
      }
      // 后台对 submit 是「立即回执」(抓取在其后台继续),所以这里等的是受理回执,
      // 给一个兜底超时即可 —— 后台未运行时给提示而不是永久卡在「正在提交…」。
      return xdexExportBridgeSend(bridge, message, XDEX_EXPORT_BRIDGE_ACK_TIMEOUT_MS);
    }
    return Promise.reject(new Error('导出需要扩展版 X岛-EX 或脚本猫桥接脚本'));
  }

  // 解析用户输入的串号:支持 `67024789`、`No.67024789`、URL、`67024789?r=x&page=23`,
  // 逗号/换行/空格分隔批量。返回 { tid, raw, ok, error }
  function xdexExportParseTidList(input) {
    const parts = String(input == null ? '' : input)
      .split(/[,\n\uFF0C;;]+/)
      .map((t) => t.trim())
      .filter(Boolean);
    return parts.map((raw) => {
      let text = raw;
      // 完整 URL:取路径中的 /t/{id} 或 /Forum/po/id/{id}
      const urlMatch = text.match(/\/t\/(\d{4,9})/) || text.match(/\/Forum\/po\/id\/(\d{4,9})/);
      if (urlMatch) {
        return { tid: urlMatch[1], raw, ok: true, error: '' };
      }
      // 去掉 No. / # 前缀,去掉 ?query 与 &page= 尾部
      text = text.replace(/^(?:No\.?|#|#)\s*/i, '').split(/[?&\s]/)[0];
      // 允许 `67024789-67024790` 之类只取首段
      const numMatch = text.match(/^(\d{4,9})/);
      if (numMatch) {
        return { tid: numMatch[1], raw, ok: true, error: '' };
      }
      return { tid: '', raw, ok: false, error: '无法识别的串号' };
    });
  }

  function xdexExportCurrentTid() {
    try {
      const m = String(location.pathname || '').match(/\/(?:t|Forum\/po\/id)\/(\d{4,9})/);
      return m ? m[1] : '';
    } catch (e) { return ''; }
  }

  function xdexExportEscapeHtml(text) {
    return String(text == null ? '' : text)
      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
  }

  // ---- 缩略进度条(sp_btn 下方导出按钮之下)----
  // 任务进度百分比与状态文案(缩略条主行与展开列表共用)
  // 任务本次的「计划页数」:提交 1-10 时是 10,而不是该串的总页数 323。
  //   to > 0 → 区间长度;to === 0(全部页哨兵)→ 用该串总页数减去起始页偏移。
  function xdexExportMiniPlannedPages(task) {
    const from = Math.max(1, Number(task.from) || 1);
    const to = Math.max(0, Number(task.to) || 0);
    const total = Number(task.totalPages) || 0;
    if (to > 0) return Math.max(0, to - from + 1);
    if (total > 0) return Math.max(0, total - from + 1);
    return 0;
  }

  function xdexExportMiniPercent(task) {
    const done = (task.progress && task.progress.donePages) ? task.progress.donePages.length : 0;
    // 分母必须是「本次计划页数」。三层来源,逐级回退:
    //   1) progress.totalPages(后台抓到首页后 / 新版入队时写入)
    //   2) 由提交区间(task.from/to)推算
    // 注意:绝不能用 task.totalPages —— 那是「该串的总页数」,会把 1-10 显示成 0/323。
    const planned = Number(task.progress && task.progress.totalPages)
      || xdexExportMiniPlannedPages(task)
      || Number(task.progress && task.progress.totalPages) /* 0 → 保持 0 */;
    return { done, total: planned, percent: planned > 0 ? Math.min(100, Math.round((done / planned) * 100)) : 0 };
  }
  function xdexExportMiniLabel(task) {
    const { done, total } = xdexExportMiniPercent(task);
    if (task.status === 'done') return '已完成';
    if (task.status === 'failed') return task.error ? `失败:${task.error}` : '失败';
    if (task.status === 'canceled') return `已取消(已导出 ${done} 页)`;
    return `${done}/${total || '?'}`;
  }
  // 任务标题:优先任务自带 title,退回串号
  function xdexExportMiniTitle(task) {
    const t = String(task.title || '').trim();
    return t && t !== '无标题' ? t : `串${task.tid}`;
  }

  function xdexExportRenderMini(task, moreCount) {
    const mini = document.getElementById('xdex-export-mini');
    if (!mini) return;
    if (!task) {
      mini.style.display = 'none';
      mini.innerHTML = '';
      return;
    }
    const { percent } = xdexExportMiniPercent(task);
    mini.style.display = 'block';
    mini.innerHTML = ''
      + `<div class="xdex-export-mini-head">`
      + `<span class="xdex-export-mini-title" title="${xdexExportEscapeHtml(xdexExportMiniTitle(task))}">${xdexExportEscapeHtml(xdexExportMiniTitle(task))}</span>`
      + `<span class="xdex-export-mini-tid">No.${xdexExportEscapeHtml(task.tid)}</span>`
      + `<button type="button" class="xdex-export-mini-btn" data-act="expand" title="${moreCount > 0 ? (xdexExportMiniExpanded ? '收起' : '展开全部任务') : '仅一个任务'}">${moreCount > 0 ? '+' + moreCount + ' ' : ''}${xdexExportMiniExpanded && moreCount > 0 ? '▴' : '▾'}</button>`
      + `<button type="button" class="xdex-export-mini-btn" data-act="cancel" title="取消任务(已下载的页会照常导出)">×</button></div>`
      + `<div class="xdex-export-mini-track"><div class="xdex-export-mini-fill" style="width:${percent}%"></div></div>`
      + `<div class="xdex-export-mini-label">${xdexExportEscapeHtml(xdexExportMiniLabel(task))}</div>`;
  }

  // 缩略条展开态(轮询重渲染时保持,否则每秒会被 innerHTML 重写折叠)
  let xdexExportMiniExpanded = false;

  function xdexExportRenderMiniList(tasks) {
    const mini = document.getElementById('xdex-export-mini');
    if (!mini) return;
    if (!tasks || !tasks.length) { xdexExportMiniExpanded = false; xdexExportRenderMini(null, 0); return; }
    const active = tasks.filter((t) => t.status === 'running' || t.status === 'queued');
    const list = active.length ? active : tasks.slice(0, 1);
    // 多任务:显示第一个,点击「+N ▾」展开其余
    xdexExportRenderMini(list[0], Math.max(0, list.length - 1));
    const expandBtn = mini.querySelector('[data-act="expand"]');
    if (expandBtn && list.length > 1) {
      expandBtn.addEventListener('click', () => {
        xdexExportMiniExpanded = !xdexExportMiniExpanded;
        xdexExportRenderMiniList(tasks);
      });
    }
    // 保持展开态(重渲染后重新挂载展开列表)
    if (xdexExportMiniExpanded && list.length > 1) {
      xdexExportRenderMiniMore(mini, list);
    }
    const cancelBtn = mini.querySelector('[data-act="cancel"]');
    if (cancelBtn) {
      cancelBtn.addEventListener('click', () => {
        xdexExportSend({ type: 'xdex:export:cancel', details: { id: list[0].id } })
          .then(() => xdexExportRefreshState())
          .catch(() => {});
      });
    }
  }

  // 展开列表:每个任务都显示「标题 + 串号 + 进度条 + 状态」,并各自带取消按钮
  function xdexExportRenderMiniMore(mini, tasks) {
    const existing = mini.querySelector('.xdex-export-mini-more');
    if (existing) existing.remove();
    const more = document.createElement('div');
    more.className = 'xdex-export-mini-more';
    tasks.slice(1).forEach((task) => {
      const { percent } = xdexExportMiniPercent(task);
      const title = xdexExportMiniTitle(task);

      const row = document.createElement('div');
      row.className = 'xdex-export-mini-more-row';

      // 头部:标题 + 串号 + 取消按钮(全部用 DOM 构造,避免 innerHTML 后再查询)
      const head = document.createElement('div');
      head.className = 'xdex-export-mini-more-head';
      const titleEl = document.createElement('span');
      titleEl.className = 'xdex-export-mini-more-title';
      titleEl.textContent = title;
      titleEl.title = title;
      const tidEl = document.createElement('span');
      tidEl.className = 'xdex-export-mini-more-tid';
      tidEl.textContent = `No.${task.tid}`;
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'xdex-export-mini-btn';
      btn.title = '取消任务(已下载的页会照常导出)';
      btn.textContent = '×';
      btn.addEventListener('click', () => {
        xdexExportSend({ type: 'xdex:export:cancel', details: { id: task.id } })
          .then(() => xdexExportRefreshState())
          .catch(() => {});
      });
      head.appendChild(titleEl);
      head.appendChild(tidEl);
      head.appendChild(btn);

      // 进度条
      const track = document.createElement('div');
      track.className = 'xdex-export-mini-more-track';
      const fill = document.createElement('div');
      fill.className = 'xdex-export-mini-more-fill';
      fill.style.width = `${percent}%`;
      track.appendChild(fill);

      // 状态文案
      const label = document.createElement('div');
      label.className = 'xdex-export-mini-more-label';
      label.textContent = xdexExportMiniLabel(task);

      row.appendChild(head);
      row.appendChild(track);
      row.appendChild(label);
      more.appendChild(row);
    });
    mini.appendChild(more);
  }

  // 进度轮询按需启停:无在跑任务即停止,避免在未使用导出功能时长期空转。
  let xdexExportIdleTicks = 0;
  let xdexExportStallSig = '';
  let xdexExportStallSince = 0;
  // 僵死判定阈值:按「单页最坏耗时」的 2 倍取值。
  //   fetchText 超时 20s,且每页最多按顺序试 3 个 Cookie 来源 → 单页最坏 60s;
  //   正常下载时状态约每 0.3~1s 就变一次(每抓完一页写一次进度)。
  //   因此「连续 2 分钟毫无变化」对正常下载是不可能的,可据此判定僵死。
  const XDEX_EXPORT_STALL_MS = 2 * 60 * 1000;
  function xdexExportEnsureProgressPolling() {
    if (xdexExportPollTimer) return;
    let inFlight = false;
    xdexExportPollTimer = setInterval(() => {
      if (document.hidden || inFlight) return;
      // 首次未知(本页尚未取过状态)→ 探测一次;之后按是否有在跑任务决定是否继续
      const st = xdexExportLastState;
      const active = (st === null) || !!(st.tasks && st.tasks.length);
      if (!active) {
        xdexExportIdleTicks += 1;
        if (xdexExportIdleTicks >= 2) {
          clearInterval(xdexExportPollTimer);
          xdexExportPollTimer = null;
          xdexExportIdleTicks = 0;
        }
        return;
      }
      xdexExportIdleTicks = 0;
      // 僵死保护:任务表连续 XDEX_EXPORT_STALL_MS 毫无变化则停止轮询
      const sig = (st.tasks || []).map((t) => `${t.id}:${t.status}:${(t.progress && t.progress.donePages ? t.progress.donePages.length : 0)}`).join('|');
      if (sig !== xdexExportStallSig) {
        xdexExportStallSig = sig;
        xdexExportStallSince = Date.now();
      } else if (Date.now() - xdexExportStallSince > XDEX_EXPORT_STALL_MS) {
        // 判定僵死:停止轮询,并清掉缩略条 —— 否则会一直显示一个永远不动的任务
        clearInterval(xdexExportPollTimer);
        xdexExportPollTimer = null;
        xdexExportLastState = null;
        try { xdexExportRenderMiniList([]); } catch (_e) { /* 忽略 */ }
        console.warn('[X岛-EX 导出] 任务进度长时间无变化,已停止轮询并隐藏进度条');
        return;
      }
      inFlight = true;
      Promise.resolve(xdexExportRefreshState()).catch(() => {}).then(() => { inFlight = false; });
    }, 3000);
  }

  let xdexExportPollTimer = null;
  let xdexExportLastState = null;

  async function xdexExportRefreshState() {
    if (!xdexExportRuntimeAvailable()) return;
    try {
      const state = await xdexExportSend({ type: 'xdex:export:list' });
      if (!state) return;
      xdexExportLastState = state;
      xdexExportRenderMiniList(state.tasks || []);
      xdexExportRenderDialogTasks(state.tasks || []);
      // 弹窗历史 = 任务表 history(与导出页「历史任务」同源),直接渲染
      try { xdexExportRenderDialogHistory(state.history || []); } catch (e) { /* 忽略 */ }
    } catch (e) { /* 扩展上下文失效时静默 */ }
  }

  // 把弹窗里的串号元素接入引用体系:
  //   渲染为 <font color="#789922">No.xxx</font>(与原站引用号同构)并包进 .h-threads-content 容器,
  //   再对该容器调用 initExtendedContent —— 由此获得与串内引用一致的行为:
  //   悬停原生引用浮窗、点击展开拓展引用浮窗、可用性检测等。
  function xdexExportAttachQuoteRef(el, tid) {
    if (!el) return;
    el.innerHTML = '';
    const container = document.createElement('div');
    container.className = 'h-threads-content';
    container.style.display = 'inline';
    const font = document.createElement('font');
    font.setAttribute('color', '#789922');
    font.textContent = `No.${tid}`;
    container.appendChild(font);
    el.appendChild(container);
    try {
      if (typeof initExtendedContent === 'function') initExtendedContent(el);
      else if (typeof extendQuote === 'function') extendQuote(el);
    } catch (e) { /* 引用功能不可用时静默(显示为纯文本链接色) */ }
  }

  // 弹窗内的任务队列:与缩略条同源(state.tasks 中 running/queued 的任务)。
  // 头部一行呈现:左「任务队列」/ 中「当前任务 + 进度」/ 右「任务总数」,可折叠。
  function xdexExportRenderDialogTasks(tasks) {
    const box = document.getElementById('xdex-export-dialog-tasks');
    if (!box) return;
    const list = (tasks || []).filter((t) => t && (t.status === 'running' || t.status === 'queued'));
    const statusEl = document.getElementById('xdex-export-queue-status');
    const countEl = document.getElementById('xdex-export-queue-count');
    if (countEl) countEl.textContent = list.length ? `共 ${list.length} 个` : '';
    if (statusEl) {
      const cur = list.find((t) => t.status === 'running') || list[0];
      if (cur) {
        const { done, total } = xdexExportMiniPercent(cur);
        statusEl.textContent = `${xdexExportMiniTitle(cur)} ${done}/${total || '?'}`;
      } else {
        statusEl.textContent = '暂无进行中的任务';
      }
    }
    if (!list.length) {
      box.textContent = '暂无进行中的任务';
      box.className = 'xdex-export-dialog-tasks empty';
      return;
    }
    box.className = 'xdex-export-dialog-tasks';
    box.innerHTML = '';
    list.forEach((task) => {
      const { percent } = xdexExportMiniPercent(task);
      const row = document.createElement('div');
      row.className = 'xdex-export-dialog-task';

      const head = document.createElement('div');
      head.className = 'xdex-export-dialog-task-head';
      const titleEl = document.createElement('span');
      titleEl.className = 'xdex-export-dialog-task-title';
      titleEl.textContent = xdexExportMiniTitle(task);
      titleEl.title = titleEl.textContent;
      const tidEl = document.createElement('span');
      tidEl.className = 'xdex-export-dialog-task-tid';
      xdexExportAttachQuoteRef(tidEl, task.tid);
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'xdex-export-dialog-btn tiny';
      btn.textContent = '中止';
      btn.title = '取消任务(已下载的页会照常导出)';
      btn.addEventListener('click', () => {
        xdexExportSend({ type: 'xdex:export:cancel', details: { id: task.id } })
          .then(() => xdexExportRefreshState())
          .catch(() => {});
      });
      head.appendChild(titleEl);
      head.appendChild(tidEl);
      head.appendChild(btn);

      const track = document.createElement('div');
      track.className = 'xdex-export-dialog-task-track';
      const fill = document.createElement('div');
      fill.className = 'xdex-export-dialog-task-fill';
      fill.style.width = `${percent}%`;
      track.appendChild(fill);

      const meta = document.createElement('div');
      meta.className = 'xdex-export-dialog-task-meta';
      meta.textContent = xdexExportMiniLabel(task);

      row.appendChild(head);
      row.appendChild(track);
      row.appendChild(meta);
      box.appendChild(row);
    });
  }

  // ---- 提交弹窗 ----
  // 弹窗状态:关闭后保留,重开时恢复(串号输入、格式、页码范围、只看PO、检索结果、状态文案)
  const xdexExportDialogState = {
    tid: '',
    format: 'md',
    from: '',
    to: '',
    poOnly: false,
    probed: new Map(),
    probeText: '',
    statusText: '',
    statusTone: '',
    queueExpanded: false,   // 任务队列默认折叠(与历史记录一致)
    historyExpanded: false  // 历史记录默认折叠,避免每次打开都占很大篇幅
  };

  // 弹窗内的历史记录:**直接复用任务表的 history**(与扩展导出页「历史任务」同一份数据),
  // 因此天然同步:任务完成自动出现、导出页删除/清空后弹窗下次刷新即一致。
  // 展示为同款式(标题 + 串号 + 状态 + 删除按钮),默认折叠。
  function xdexExportRenderDialogHistory(list) {
    const box = document.getElementById('xdex-export-dialog-history');
    if (!box) return;
    const items = Array.isArray(list) ? list : [];
    const countEl = document.getElementById('xdex-export-history-count');
    if (countEl) countEl.textContent = items.length ? `共 ${items.length} 条` : '';
    box.className = 'xdex-export-dialog-tasks' + (items.length ? '' : ' empty');
    if (!items.length) {
      box.textContent = '(暂无历史)';
      return;
    }
    box.innerHTML = '';
    const statusMap = { done: '已完成', failed: '失败', canceled: '已取消', interrupted: '已中断' };
    items.forEach((task) => {
      const row = document.createElement('div');
      row.className = 'xdex-export-dialog-task';

      const head = document.createElement('div');
      head.className = 'xdex-export-dialog-task-head';
      const titleEl = document.createElement('span');
      titleEl.className = 'xdex-export-dialog-task-title';
      titleEl.textContent = xdexExportMiniTitle(task);
      titleEl.title = titleEl.textContent;
      const tidEl = document.createElement('span');
      tidEl.className = 'xdex-export-dialog-task-tid';
      xdexExportAttachQuoteRef(tidEl, task.tid);
      const del = document.createElement('button');
      del.type = 'button';
      del.className = 'xdex-export-dialog-btn tiny';
      del.textContent = '删除';
      del.title = '删除这条历史记录';
      del.addEventListener('click', () => {
        // 与导出页「删除记录」同一条消息:任务表 history 删除后两处一致
        xdexExportSend({ type: 'xdex:export:remove-history', details: { id: task.id } })
          .then(() => xdexExportRefreshState())
          .catch(() => {});
      });
      head.appendChild(titleEl);
      head.appendChild(tidEl);
      head.appendChild(del);

      const meta = document.createElement('div');
      meta.className = 'xdex-export-dialog-task-meta';
      const parts = [statusMap[task.status] || task.status || ''];
      if (task.filename) parts.push(task.filename);
      else if (task.error) parts.push(task.error);
      if (task.finishedAt) parts.push(new Date(task.finishedAt).toLocaleString());
      meta.textContent = parts.filter(Boolean).join(' ');

      row.appendChild(head);
      row.appendChild(meta);
      box.appendChild(row);
    });
  }


  function xdexExportOpenDialog(prefillTid) {
    if (!xdexExportRuntimeAvailable()) {
      toast('导出需安装扩展版 X岛-EX,或脚本猫桥接脚本', 2400, { type: 'error', key: 'export-needs-extension' });
      return;
    }
    const tid = prefillTid || xdexExportCurrentTid();
    if (document.getElementById('xdex-export-dialog')) return;
    const dialog = document.createElement('div');
    dialog.id = 'xdex-export-dialog';
    dialog.innerHTML = ''
      + '<div class="xdex-export-dialog-panel">'
      + '  <div class="xdex-export-dialog-head"><span>导出串</span>'
      + '    <span class="xdex-export-dialog-head-btns">'
      + '      <button type="button" class="xdex-export-dialog-icon" data-act="min" title="最小化">−</button>'
      + '      <button type="button" class="xdex-export-dialog-icon" data-act="close" title="关闭(任务继续)">×</button>'
      + '    </span></div>'
      + '  <div class="xdex-export-dialog-body">'
      + '    <div class="xdex-export-dialog-row">'
      + `      <input id="xdex-export-tid" type="text" placeholder="串号 / No.67024789 / 链接,可逗号批量" value="${xdexExportEscapeHtml(tid)}">`
      + '      <button type="button" class="xdex-export-dialog-btn" data-act="probe">检索</button>'
      + '    </div>'
      + '    <div id="xdex-export-probe" class="xdex-export-dialog-info"></div>'
      + '    <div class="xdex-export-dialog-row">'
      + '      <span class="xdex-export-dialog-label">总页码</span><span id="xdex-export-total" class="xdex-export-dialog-value">—</span>'
      + '      <span class="xdex-export-dialog-label">格式</span>'
      + '      <select id="xdex-export-format"><option value="md">Markdown</option><option value="txt">TXT</option></select>'
      + '    </div>'
      + '    <div class="xdex-export-dialog-row">'
      + '      <span class="xdex-export-dialog-label">下载</span>'
      + '      <input id="xdex-export-from" type="number" min="1" placeholder="起始" class="xdex-export-dialog-num">'
      + '      <span>-</span>'
      + '      <input id="xdex-export-to" type="number" min="1" placeholder="结束" class="xdex-export-dialog-num">'
      + '      <button type="button" class="xdex-export-dialog-btn primary" data-act="submit">开始</button>'
      + '      <label class="xdex-export-dialog-switch" for="xdex-export-po" title="只导出主串(PO)自己的发言">'
      + '        <input id="xdex-export-po" type="checkbox" class="xdex-switch" role="switch"><span>只看 PO</span></label>'
      + '    </div>'
      + '    <div id="xdex-export-status" class="xdex-export-dialog-info"></div>'
      + '    <div class="xdex-export-dialog-queue">'
      + '      <div class="xdex-export-dialog-queue-head" data-act="toggle-queue" role="button" tabindex="0">'
      + '        <span class="q-title">任务队列</span>'
      + '        <span class="q-status" id="xdex-export-queue-status">暂无进行中的任务</span>'
      + '        <span class="q-count" id="xdex-export-queue-count"></span>'
      + '        <span class="q-caret" id="xdex-export-queue-caret"></span>'
      + '      </div>'
      + '      <div id="xdex-export-dialog-tasks" class="xdex-export-dialog-tasks"></div>'
      + '    </div>'
      + '    <div class="xdex-export-dialog-queue collapsed" id="xdex-export-dialog-history-box">'
      + '      <div class="xdex-export-dialog-queue-head" data-act="toggle-history" role="button" tabindex="0">'
      + '        <span class="q-title">历史记录</span>'
      + '        <span class="q-count" id="xdex-export-history-count"></span>'
      + '        <span class="q-caret" id="xdex-export-history-caret"></span>'
      + '      </div>'
      + '      <div id="xdex-export-dialog-history" class="xdex-export-dialog-tasks"></div>'
      + '    </div>'
      + '  </div>'
      + '</div>';
    document.body.appendChild(dialog);

    const q = (sel) => dialog.querySelector(sel);
    const status = (text, tone) => {
      const el = q('#xdex-export-status');
      el.textContent = text || '';
      el.dataset.tone = tone || '';
      xdexExportDialogState.statusText = text || '';
      xdexExportDialogState.statusTone = tone || '';
    };
    // 已检索通过的串号集合:跨弹窗开关复用(存在模块级状态里)
    const probed = xdexExportDialogState.probed;

    // 回填上次的输入与结果,避免每次打开都从零开始
    try {
      const st = xdexExportDialogState;
      if (st.format) q('#xdex-export-format').value = st.format;
      if (st.from) q('#xdex-export-from').value = st.from;
      if (st.to) q('#xdex-export-to').value = st.to;
      q('#xdex-export-po').checked = !!st.poOnly;
      if (st.probeText) {
        q('#xdex-export-probe').textContent = st.probeText;
      } else {
        // 未检索前给一句说明,避免信息栏空着显得突兀
        const ph = q('#xdex-export-probe');
        ph.textContent = '填写串号后点「检索」,这里会显示每个串的标题、总页数与回复数(支持 No.67024789 / 链接 / 逗号批量)';
        ph.dataset.tone = 'placeholder';
      }
      if (st.statusText) status(st.statusText, st.statusTone);
      if (probed.size === 1) q('#xdex-export-total').textContent = String(Array.from(probed.values())[0].totalPages);
      else if (probed.size > 1) q('#xdex-export-total').textContent = `${probed.size} 个串`;
      xdexExportRenderDialogTasks(xdexExportLastState && xdexExportLastState.tasks);
      if (xdexExportExtensionAvailable()) {
        // 扩展环境:历史在任务表(chrome.storage),拉一次最新
        xdexExportSend({ type: 'xdex:export:list' }).then((st) => {
          if (st) { xdexExportLastState = st; xdexExportRenderDialogHistory(st.history || []); }
        }).catch(() => {});
      } else {
        // 脚本猫端:历史同样在后台任务表里。本页可能还没跑过 list(按需轮询是空的),
        // 所以也要主动取一次,否则「修改前」由其它页面/会话产生的记录看不到。
        xdexExportSend({ type: 'xdex:export:list' }).then((st) => {
          if (st) { xdexExportLastState = st; xdexExportRenderDialogHistory(st.history || []); }
        }).catch(() => {
          // 后台未运行时退回最近一次已知状态
          xdexExportRenderDialogHistory(xdexExportLastState && xdexExportLastState.history);
        });
      }
    } catch (_e) { /* 忽略 */ }

    // 输入变化即写回状态
    const rememberInputs = () => {
      const st = xdexExportDialogState;
      st.tid = q('#xdex-export-tid').value;
      st.format = q('#xdex-export-format').value;
      st.from = q('#xdex-export-from').value;
      st.to = q('#xdex-export-to').value;
      st.poOnly = q('#xdex-export-po').checked;
    };
    ['#xdex-export-tid', '#xdex-export-format', '#xdex-export-from', '#xdex-export-to', '#xdex-export-po'].forEach((sel) => {
      const el = q(sel);
      if (!el) return;
      el.addEventListener('input', rememberInputs);
      el.addEventListener('change', rememberInputs);
    });

    const closeDialog = () => { rememberInputs(); dialog.remove(); };
    // 点击遮罩(面板以外区域)关闭
    dialog.addEventListener('click', (e) => { if (e.target === dialog) closeDialog(); });

    // 任务队列折叠(参照设置面板 WebDAV 行的折叠交互)
    const queueBox = q('.xdex-export-dialog-queue');
    const applyQueueExpanded = () => {
      if (!queueBox) return;
      const expanded = !!xdexExportDialogState.queueExpanded;
      queueBox.classList.toggle('collapsed', !expanded);
      // 与「常用串EX」同一个箭头函数:展开→向下,收起→向左
      try {
        if (typeof setPostExpandToggleVisual === 'function') {
          setPostExpandToggleVisual(q('#xdex-export-queue-caret'), expanded);
        }
      } catch (_e) { /* 忽略 */ }
    };
    if (queueBox) {
      q('[data-act="toggle-queue"]').addEventListener('click', () => {
        xdexExportDialogState.queueExpanded = !xdexExportDialogState.queueExpanded;
        applyQueueExpanded();
      });
    }
    applyQueueExpanded();

    // 历史记录折叠(默认折叠;箭头与任务队列同款)
    const historyBox = q('#xdex-export-dialog-history-box');
    const applyHistoryExpanded = () => {
      if (!historyBox) return;
      const expanded = !!xdexExportDialogState.historyExpanded;
      historyBox.classList.toggle('collapsed', !expanded);
      try {
        if (typeof setPostExpandToggleVisual === 'function') {
          setPostExpandToggleVisual(q('#xdex-export-history-caret'), expanded);
        }
      } catch (_e) { /* 忽略 */ }
    };
    if (historyBox) {
      q('[data-act="toggle-history"]').addEventListener('click', () => {
        xdexExportDialogState.historyExpanded = !xdexExportDialogState.historyExpanded;
        applyHistoryExpanded();
      });
    }
    applyHistoryExpanded();

    q('[data-act="close"]').addEventListener('click', closeDialog);
    q('[data-act="min"]').addEventListener('click', closeDialog);

    // 范围输入:留空即全部页(from=1, to=0)
    const readRange = () => {
      const from = Math.max(0, Number(q('#xdex-export-from').value) || 0);
      const to = Math.max(0, Number(q('#xdex-export-to').value) || 0);
      return { from: from || 1, to: to || 0 };
    };

    // 校验用户填写的页码区间是否真实存在(依据检索到的总页数)。
    // 只填一端时另一端自动补全:只填起始 → 到最后一页;只填结束 → 从第 1 页。
    const validateRange = (info) => {
      const rawFrom = q('#xdex-export-from').value.trim();
      const rawTo = q('#xdex-export-to').value.trim();
      const total = Number(info && info.totalPages) || 0;
      if (!rawFrom && !rawTo) return { ok: true, from: 1, to: 0, all: true };
      const fromN = Number(rawFrom);
      const toN = Number(rawTo);
      if (rawFrom && (!Number.isFinite(fromN) || fromN < 1)) return { ok: false, error: '起始页必须是 ≥1 的整数' };
      if (rawTo && (!Number.isFinite(toN) || toN < 1)) return { ok: false, error: '结束页必须是 ≥1 的整数' };
      const from = rawFrom ? Math.floor(fromN) : 1;
      const to = rawTo ? Math.floor(toN) : (total || 0);
      if (total && from > total) return { ok: false, error: `起始页 ${from} 超出总页数 ${total}` };
      if (total && to > total) return { ok: false, error: `结束页 ${to} 超出总页数 ${total}` };
      if (to && to < from) return { ok: false, error: `结束页 ${to} 小于起始页 ${from}` };
      return { ok: true, from, to, all: false };
    };

    q('[data-act="probe"]').addEventListener('click', async () => {
      const list = xdexExportParseTidList(q('#xdex-export-tid').value.trim() || xdexExportCurrentTid());
      if (!list.length) { status('请输入串号', 'error'); return; }
      probed.clear();
      status('正在检索…');
      const oks = [];
      const fails = [];
      for (const item of list) {
        if (!item.ok) { fails.push(`${item.raw}:${item.error}`); continue; }
        try {
          const info = await xdexExportSend({ type: 'xdex:export:probe', details: { tid: item.tid } });
          probed.set(item.tid, info);
          oks.push(`✓《${info.title || `串${item.tid}`}》 ${item.tid} 共 ${info.totalPages} 页 / ${info.totalReplies} 回复`);
        } catch (e) {
          fails.push(`✗ ${item.tid} 不可下载:${e.message}`);
        }
      }
      const probeEl = q('#xdex-export-probe');
      probeEl.textContent = oks.concat(fails).join('\n');
      probeEl.dataset.tone = fails.length ? 'error' : 'ok';
      xdexExportDialogState.probeText = probeEl.textContent;
      if (probed.size === 1) {
        const only = Array.from(probed.values())[0];
        q('#xdex-export-total').textContent = String(only.totalPages);
      } else {
        q('#xdex-export-total').textContent = probed.size ? `${probed.size} 个串` : '—';
      }
      status(fails.length ? `${fails.length} 个串不可下载,其余可正常加入` : `检索完成:${oks.length} 个串可下载`, fails.length ? 'error' : 'ok');
    });

    q('[data-act="submit"]').addEventListener('click', async () => {
      const list = xdexExportParseTidList(q('#xdex-export-tid').value.trim() || xdexExportCurrentTid());
      if (!list.length) { status('请输入串号', 'error'); return; }
      const bothEmpty = !q('#xdex-export-from').value.trim() && !q('#xdex-export-to').value.trim();
      const format = q('#xdex-export-format').value;
      const poOnly = q('#xdex-export-po').checked;
      // 已经检索过的不再重复检索:只对「尚未检索」的串补检
      const listOk = list.filter((x) => x.ok);
      const needProbe = listOk.filter((x) => !probed.get(x.tid));
      status(needProbe.length ? `正在检索 ${needProbe.length} 个未检索的串…` : '正在提交…');
      const done = [];
      const fails = [];
      const infos = [];
      for (const item of list) {
        if (!item.ok) { fails.push(`${item.raw}:${item.error}`); continue; }
        // 复用已有检索结果;没有才去检索
        let info = probed.get(item.tid);
        if (!info) {
          try {
            info = await xdexExportSend({ type: 'xdex:export:probe', details: { tid: item.tid } });
            probed.set(item.tid, info);
          } catch (e) { fails.push(`${item.tid} 不可下载:${e.message}`); continue; }
        }
        // 页码区间校验(依据该串的总页数)
        const chk = validateRange(info);
        if (!chk.ok) { fails.push(`${item.tid} 页码范围无效:${chk.error}`); continue; }
        if (bothEmpty || chk.all) {
          infos.push(`《${info.title || `串${item.tid}`}》 ${item.tid} 共 ${info.totalPages} 页 / ${info.totalReplies} 回复 → 全量导出`);
        }
        try {
          await xdexExportSend({
            type: 'xdex:export:submit',
            details: {
              tid: item.tid,
              title: info.title || '',
              fid: info.fid || '',
              totalPages: info.totalPages || 0,
              from: chk.from,
              to: chk.to,
              format,
              poOnly
            }
          });
          done.push(`${item.tid}(${chk.from}-${chk.all ? info.totalPages : chk.to} 页)`);
        } catch (e) { fails.push(`${item.tid} 提交失败:${e.message}`); }
      }
      const parts = [];
      if (infos.length) parts.push(infos.join('\n'));
      if (done.length) parts.push(`已加入导出队列 ${done.length} 个:${done.join('、')}`);
      if (fails.length) parts.push(`失败 ${fails.length} 个:${fails.join(';')}`);
      status(parts.join('|'), fails.length ? 'error' : 'ok');
      if (infos.length) q('#xdex-export-probe').textContent = infos.join('\n');
      xdexExportIdleTicks = 0;
      xdexExportEnsureProgressPolling();
      await xdexExportRefreshState();
    });
  }

  // ---- 按钮 + 样式注入 ----
  function xdexExportInstallUi() {
    if (!xdexExportRuntimeAvailable()) return; // 扩展与脚本猫桥接均不可用:不注入
    if (document.getElementById('xdex-export-ui')) return;
    const style = document.createElement('style');
    style.id = 'xdex-export-ui';
    style.textContent = ''
      // 外观复用阅图模式按钮 .xdex-image-viewer-btn(+ 站点 active 类 ultimate-bold-correct),
      // 这里只覆盖定位到 sp_btn 正下方
      + '#xdex-export-btn{top:54px;right:10px;}'
      + '#xdex-export-mini{position:fixed;top:96px;right:10px;z-index:10000;width:172px;padding:8px;border-radius:10px;'
      + 'background:rgba(255,255,238,.97);border:1px solid #bfa58f;box-shadow:0 2px 10px rgba(0,0,0,.18);display:none;'
      + 'font:12px/1.5 "Microsoft YaHei",sans-serif;color:#4a3a30;}'
      + '.xdex-export-mini-head{display:flex;align-items:center;gap:4px;}'
      + '.xdex-export-mini-tid{flex:1;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}'
      + '.xdex-export-mini-track{height:6px;border-radius:999px;background:rgba(143,74,47,.15);overflow:hidden;margin:5px 0 3px;}'
      + '.xdex-export-mini-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,#8f4a2f,#1f6f74);transition:width .2s ease;}'
      + '.xdex-export-mini-label{color:#8a7566;text-align:right;}'
      + '.xdex-export-mini-title{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:96px;}'
      // 展开列表:整块纵向排列(标题行 / 进度条 / 状态),每行之间留白
      + '.xdex-export-mini-more{margin-top:6px;padding-top:6px;border-top:1px dashed #bfa58f;display:flex;flex-direction:column;gap:9px;max-height:46vh;overflow:auto;}'
      + '.xdex-export-mini-more-row{display:block;}'
      + '.xdex-export-mini-more-head{display:flex;align-items:center;gap:4px;}'
      + '.xdex-export-mini-more-title{flex:1;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}'
      + '.xdex-export-mini-more-tid{color:#8a7566;white-space:nowrap;font-size:11.5px;}'
      + '.xdex-export-mini-more-track{height:5px;border-radius:999px;background:rgba(143,74,47,.15);overflow:hidden;margin:4px 0 2px;}'
      + '.xdex-export-mini-more-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,#8f4a2f,#1f6f74);transition:width .2s ease;}'
      + '.xdex-export-mini-more-label{color:#8a7566;text-align:right;font-size:11.5px;}'
      // 缩略条的展开/中止按钮:做成有边框的小胶囊,提升可点性与辨识度
      + '.xdex-export-mini-btn{min-width:20px;height:18px;padding:0 5px;border:1px solid #bfa58f;border-radius:999px;background:#fffaf4;cursor:pointer;'
      + 'color:#683520;font:11px/1 "Microsoft YaHei",sans-serif;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;}'
      + '.xdex-export-mini-btn:hover{background:#f0e0d6;border-color:#8f4a2f;}'
      + '.xdex-export-mini-btn[data-act="cancel"]{color:#9d3026;border-color:#d9a79f;}'
      + '.xdex-export-mini-btn[data-act="cancel"]:hover{background:#f7e2df;border-color:#9d3026;}'
      + '.xdex-export-mini-btn[data-act="expand"]{font-weight:700;}'
      + '#xdex-export-dialog{position:fixed;inset:0;z-index:10001;background:rgba(0,0,0,.35);display:flex;align-items:flex-start;justify-content:center;padding-top:12vh;}'
      + '.xdex-export-dialog-panel{width:min(680px,calc(100vw - 32px));background:#FFFFEE;border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.28);overflow:hidden;font:14px/1.6 "Microsoft YaHei",sans-serif;color:#2b211c;}'
      + '.xdex-export-dialog-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#F0E0D6;font-weight:700;}'
      + '.xdex-export-dialog-head-btns{display:flex;gap:6px;}'
      + '.xdex-export-dialog-icon{border:none;background:transparent;cursor:pointer;font:16px/1 "Microsoft YaHei",sans-serif;color:#8a7566;padding:2px 6px;border-radius:6px;}'
      + '.xdex-export-dialog-icon:hover{background:rgba(0,0,0,.08);}'
      + '.xdex-export-dialog-body{padding:14px;display:flex;flex-direction:column;gap:10px;}'
      + '.xdex-export-dialog-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}'
      + '.xdex-export-dialog-row input[type=text]{flex:1;min-width:0;padding:6px 10px;border:1px solid #bfa58f;border-radius:8px;box-sizing:border-box;}'
      + '.xdex-export-dialog-row select{padding:6px 8px;border:1px solid #bfa58f;border-radius:8px;}'
      + '.xdex-export-dialog-num{width:76px;padding:6px 8px;border:1px solid #bfa58f;border-radius:8px;box-sizing:border-box;}'
      + '.xdex-export-dialog-btn{padding:6px 14px;border:1px solid #bfa58f;border-radius:999px;background:#fff;cursor:pointer;font-weight:700;color:#683520;}'
      + '.xdex-export-dialog-btn.primary{background:#8f4a2f;border-color:#683520;color:#fffaf4;}'
      + '.xdex-export-dialog-label{color:#8a7566;}'
      + '.xdex-export-dialog-value{font-weight:700;}'
      + '.xdex-export-dialog-info{min-height:18px;font-size:12px;color:#8a7566;white-space:pre-wrap;}'
      + '.xdex-export-dialog-info[data-tone=error]{color:#9d3026;}'
      + '.xdex-export-dialog-info[data-tone=ok]{color:#1f6f74;}'
      + '.xdex-export-dialog-info[data-tone=placeholder]{color:#a2917f;font-style:italic;}'
      + '.xdex-export-dialog-check{display:flex;align-items:center;gap:6px;color:#8a7566;font-size:13px;}'
      // 「只看 PO」直接沿用设置面板的 .xdex-switch 规则:选择器与声明逐字一致,
      // 且与面板一样不做暗色覆盖 —— 两处开关必须表现完全相同。
      + '.xdex-switch { appearance:none; width:34px; height:18px; flex:0 0 34px; border:1px solid #a98f7a; border-radius:999px; background:#EE0000; cursor:pointer; position:relative; box-sizing:border-box; transition:background .18s ease,border-color .18s ease; }'
      + '.xdex-switch::before { content:""; position:absolute; width:14px; height:14px; left:1px; top:1px; border-radius:50%; background:#FFFFEE; box-shadow:0 1px 2px rgba(0,0,0,.24); transition:transform .18s ease; }'
      + '.xdex-switch:checked { background:#66CCFF; border-color:#7da6bf; }'
      + '.xdex-switch:checked::before { transform:translateX(16px); }'
      + '.xdex-switch:focus-visible { outline:2px solid #cc1105; outline-offset:2px; }'
      + '.xdex-switch:disabled { opacity:1; cursor:default; }'
      + '.xdex-export-dialog-switch{display:flex;align-items:center;gap:6px;color:#8a7566;font-size:13px;cursor:pointer;user-select:none;}'
      // 弹窗内任务队列
      + '.xdex-export-dialog-queue{margin-top:2px;border-top:1px dashed #d9c8bb;padding-top:8px;}'
      // 队列头部:参照设置面板「WebDAV 备份/同步」行的排布(左标题 / 中状态 / 右计数与折叠)
      + '.xdex-export-dialog-queue-head{display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none;padding:2px 0;}'
      + '.xdex-export-dialog-queue-head .q-title{font-weight:700;}'
      + '.xdex-export-dialog-queue-head .q-status{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8a7566;font-size:12px;text-align:center;}'
      // 历史区没有 q-status,由 q-count 吃掉剩余空间并把箭头推到最右
      + '.xdex-export-dialog-queue-head .q-count{color:#8a7566;font-size:12px;white-space:nowrap;margin-left:auto;}'
      + '.xdex-export-dialog-queue-head .q-caret{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:#8a7566;}'
      + '.xdex-export-dialog-queue-head:hover .q-caret{color:#683520;}'
      + '.xdex-export-dialog-queue-head .q-caret .xdex-icon-chevron{display:block;width:16px;height:16px;}'
      + '.xdex-export-dialog-tasks{display:flex;flex-direction:column;gap:8px;max-height:26vh;overflow:auto;margin-top:8px;}'
      + '.xdex-export-dialog-queue.collapsed .xdex-export-dialog-tasks{display:none;}'
      + '.xdex-export-dialog-tasks.empty{color:#a2917f;font-size:12px;display:flex;align-items:center;justify-content:center;padding:6px 0;}'
      + '.xdex-export-dialog-task{border:1px solid #e3d5c8;border-radius:8px;padding:6px 8px;background:#fffdf6;}'
      + '.xdex-export-dialog-task-head{display:flex;align-items:center;gap:6px;}'
      + '.xdex-export-dialog-task-title{flex:1;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}'
      + '.xdex-export-dialog-task-tid{color:#8a7566;font-size:12px;white-space:nowrap;}'
      + '.xdex-export-dialog-task-track{height:6px;border-radius:999px;background:rgba(143,74,47,.15);overflow:hidden;margin:5px 0 3px;}'
      + '.xdex-export-dialog-task-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,#8f4a2f,#1f6f74);transition:width .2s ease;}'
      + '.xdex-export-dialog-task-meta{color:#8a7566;font-size:12px;text-align:right;}'
      + '.xdex-export-dialog-btn.tiny{padding:2px 8px;font-size:12px;}'
      // ── 暗色(DarkReader 激活或脚本自定义深色):逐项覆盖,避免 DR 反转导致配色错乱 ──
      + ':root.xdex-darkreader-active #xdex-export-mini{background:rgba(43,44,45,.97);border-color:#5a4d42;color:#d9d0d0;}'
      + ':root.xdex-darkreader-active .xdex-export-mini-label,'
      + ':root.xdex-darkreader-active .xdex-export-mini-more-tid,'
      + ':root.xdex-darkreader-active .xdex-export-mini-more-label{color:#b9aeb0;}'
      + ':root.xdex-darkreader-active .xdex-export-mini-track,'
      + ':root.xdex-darkreader-active .xdex-export-mini-more-track{background:rgba(255,255,255,.14);}'
      + ':root.xdex-darkreader-active .xdex-export-mini-btn{background:#3a3b3c;border-color:#5a4d42;color:#d9d0d0;}'
      + ':root.xdex-darkreader-active .xdex-export-mini-btn:hover{background:#4a4b4c;border-color:#00ffcc;color:#00ffcc;}'
      + ':root.xdex-darkreader-active .xdex-export-mini-btn[data-act="cancel"]{color:#e08a80;border-color:#7a4a44;}'
      + ':root.xdex-darkreader-active .xdex-export-mini-btn[data-act="cancel"]:hover{background:#5a2f2a;border-color:#EE0000;color:#ff9a90;}'
      + ':root.xdex-darkreader-active .xdex-export-mini-more{border-top-color:#5a4d42;}'
      + ':root.xdex-darkreader-active #xdex-export-dialog{background:rgba(0,0,0,.55);}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-panel{background:#2b2c2d;color:#e6dede;color-scheme:dark;box-shadow:0 8px 30px rgba(0,0,0,.6);}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-head{background:#3a2f28;color:#e6dede;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-row input[type=text],'
      + ':root.xdex-darkreader-active .xdex-export-dialog-row select,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-num{background:#1f2021;border-color:#5a4d42;color:#e6dede;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-btn{background:#3a3b3c;border-color:#5a4d42;color:#d9d0d0;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-btn:hover{border-color:#00ffcc;color:#00ffcc;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-btn.primary{background:#7a3f28;border-color:#a05a3a;color:#fff5ef;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-icon{color:#b9aeb0;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-icon:hover{background:rgba(255,255,255,.12);}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-label,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-value,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-info,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-check,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-switch{color:#b9aeb0;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-info[data-tone=error]{color:#ff9a90;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-info[data-tone=ok]{color:#5fd6b0;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-info[data-tone=placeholder]{color:#8a7f80;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-queue{border-top-color:#5a4d42;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-queue-head:hover .q-caret{color:#00ffcc;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-queue-head .q-status,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-queue-head .q-count,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-queue-head .q-caret,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-subtitle,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-tasks.empty,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-task-tid,'
      + ':root.xdex-darkreader-active .xdex-export-dialog-task-meta{color:#b9aeb0;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-task{background:#232425;border-color:#3a3b3c;}'
      + ':root.xdex-darkreader-active .xdex-export-dialog-task-track{background:rgba(255,255,255,.14);}';
    document.head.appendChild(style);

    // 复用阅图模式按钮样式(确保基础样式已注入)
    if (typeof ensureImageViewerButtonStyle === 'function') ensureImageViewerButtonStyle();
    const btn = document.createElement('button');
    btn.id = 'xdex-export-btn';
    btn.type = 'button';
    btn.className = 'xdex-image-viewer-btn ultimate-bold-correct';
    btn.title = '导出串';
    btn.setAttribute('aria-label', '导出串');
    // 图标:折角文档 + 两条文字线("文档 + 内容"的语义),
    // 与阅图按钮同规范:不带 width/height,由 .xdex-image-viewer-btn svg 统一设为 18×18。
    btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" '
      + 'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'
      + '<path d="M14.5 2.5H6.5A2 2 0 0 0 4.5 4.5v15a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7.5z"/>'
      + '<path d="M14.5 2.5v5h5"/>'
      + '<path d="M8.5 12h7M8.5 16.5h4.5"/>'
      + '</svg>';
    btn.addEventListener('click', () => xdexExportOpenDialog(''));
    document.body.appendChild(btn);

    const mini = document.createElement('div');
    mini.id = 'xdex-export-mini';
    document.body.appendChild(mini);

    // 进度订阅:
    //  - 扩展环境由 chrome.storage.onChanged 驱动(实时),无需轮询;
    //  - 脚本猫桥接没有存储事件转发,只能轮询兜底。
    // 桥接下 submit 会在后台连续抓取(大串可达数分钟),1s 轮询会让后台同时处理
    // 大量 list 请求,因此间隔放宽到 3s,且同一时刻只允许一个在途请求。
    // 进度订阅:
    //  - 扩展环境由 chrome.storage.onChanged 驱动(实时),无需轮询;
    //  - 脚本猫桥接没有存储事件转发,只能轮询。
    // 关键:**没有在跑的任务时不轮询** —— 轮询一轮要经桥接往返(多次跨脚本写入、
    // 两个 800ms 兜底轮询、若干 DOM 事件),常驻空转在普通浏览时纯属浪费。
    // 仅当用户在本页提交过任务后才启动,任务清空后自动停止。
    if (!xdexExportExtensionAvailable()) {
      xdexExportEnsureProgressPolling();
      xdexExportRefreshState(); // 立即探测一次:本页可能是中途打开的,后台可能已有任务
    } else {
      // 扩展环境:任务表变化由 chrome.storage.onChanged 推送(原生机制,零轮询开销)。
      // 此前注释声称由 onChanged 驱动,但从未订阅 —— 导致扩展端弹窗进度/历史不自动更新。
      try {
        if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.onChanged) {
          chrome.storage.onChanged.addListener((changes, area) => {
            if (area !== 'local' || !changes) return;
            const hit = Object.keys(changes).some((k) => k === 'xdex-export-tasks' || k === 'xdex-export-dialog-history');
            if (hit) xdexExportRefreshState();
          });
        }
      } catch (e) { /* 忽略 */ }
      xdexExportRefreshState();
    }
  }

  // 非扩展环境:保留调用点(分支留空,等待后续实现)
  function xdexExportInit() {
    try {
      // 启动诊断:便于在页面控制台确认当前装的是哪个版本、通道是否就绪
      try {
        console.log('[X岛-EX 导出] 通道诊断',
          '扩展=' + xdexExportExtensionAvailable(),
          '桥接=' + !!xdexExportBridge(),
          '按需轮询=' + (typeof xdexExportEnsureProgressPolling === 'function'),
          '受理回执超时=' + (typeof XDEX_EXPORT_BRIDGE_ACK_TIMEOUT_MS === 'number'));
      } catch (_e) { /* 忽略 */ }
      if (xdexExportRuntimeAvailable()) { xdexExportInstallUi(); return; }
      // 桥接脚本与主脚本同为 document-start,加载顺序不确定:
      // 若桥接尚未就绪,则监听其就绪事件后补注入(已就绪的情况上面已处理)
      const onBridgeReady = () => {
        document.removeEventListener('xdex:export-bridge-ready', onBridgeReady);
        try { if (xdexExportRuntimeAvailable()) xdexExportInstallUi(); } catch (_e) {}
      };
      document.addEventListener('xdex:export-bridge-ready', onBridgeReady);
    } catch (e) { console.warn('[X岛-EX] 导出功能初始化失败', e); }
  }

  /* --------------------------------------------------
   * tag -1. 入口初始化
   * -------------------------------------------------- */
  // 注册设置面板依赖到 XDex
  Object.assign(XDex.ui, {
    buildFavoriteThreadRowHtml,
    buildSubscriptionFeedRowHtml,
    buildThreadCookieWhitelistRowHtml,
    buildBlockedKeywordGroupRowHtml,
    buildCookieGroupTwoFieldRowHtml,
    getBlockedKeywordPlaceholder,
    collectFavoriteThreadsFromPanel,
    collectSubscriptionFeedsFromPanel
  });
  Object.assign(XDex.data, {
    markColors,
    parseDescAndListByLastColon,
    isValidDesc,
    isValidHexColor,
    normalizeHexColor,
    clampColorChannel,
    rgbToHex,
    hexToRgb,
    hsvToRgb,
    rgbToHsv,
    hexToHsv,
    hsvToHex,
    parseRgbColorString,
    formatRgbColor,
    getMarkedGroupEffectiveColor,
    isValidThreadId,
    normalizeFavoriteThreadInput,
    normalizeFavoriteThreads,
    normalizeThreadCookieWhitelistGroups,
    mergeThreadCookieWhitelistGroups,
    normalizeBlockedGroups,
    normalizeBlockedKeywordGroupValue,
    isBlockedKeywordRegexGroup,
    normalizeBlockedKeywordGroups,
    flattenBlockedKeywords,
    compileBlockedKeywordRegex,
    normalizeMarkedGroups,
    escapeBlockedKeywordInputToken,
    joinBlockedKeywordInputTokens,
    isThreadIdKeyword,
    isEightDigitKeyword
  });
  Object.assign(XDex.update, {
    getUpdateCheckState,
    setUpdateCheckState,
    getDefaultUpdateCheckState,
    updateSettingsButtonBadge,
    clearFooterUpdateHighlight,
    flashFooterUpdateHighlight,
    renderUpdateLogDialog,
    openUpdateLogDialog,
    closeUpdateLogDialog,
    maybeShowPendingUpdateDialogOnPanelOpen,
    checkForDailyScriptUpdate,
    shouldShowPendingUpdateReminder,
    formatLocalDateKey,
    getNextNaturalCheckAt,
    compareVersionStrings
  });

  window.addEventListener('load', () => {
    startupPerfDebug.mark('window.load.start', startupPerfDebug.summarizeRoot(document));
    startupPerfDebug.measure('window.load.enableHDImageAndLayoutFix', () => enableHDImageAndLayoutFix(document), () => startupPerfDebug.summarizeRoot(document));
    startupPerfDebug.mark('window.load.end', startupPerfDebug.summarizeRoot(document));
  });
  installEarlyStartupObserver();
  installEarlyCriticalVisualPass();
  installEarlyEnhanceIslandAutoTitle();

  $(document).ready(() => {
    startupPerfDebug.mark('document.ready.start', startupPerfDebug.summarizeRoot(document));
    if (isNmbSearchPage()) {
      initNmbSearchMobileThreadRedirector();
      startupPerfDebug.mark('startup.nmbSearchMobileThreadRedirector', startupPerfDebug.summarizeRoot(document));
      return;
    }
    SettingPanel.init();
    const cfg = Object.assign({}, SettingPanel.defaults, GM_getValue(SettingPanel.key, {}));
    disableVerifyInputMemory(document);
    initReplyParamScrollTo();                                      //?page&r=链接定位到对应回复
    initDisableAutoQuote(); //关闭引用:阻止URL中?r=参数自动插入引用号
    replyQuicklyOnBoardPage();                                      //板块页快速回复模式切换
    if (cfg.enableCookieSwitch)          createCookieSwitcherUI();  //快捷切换饼干
    // 串内饼干偏好初始化
    if (cfg.enableCookieSwitch && cfg.enableCookieConfirm) {
      const _tid = PageType.getThreadId(false);
      const _initThreadId = _tid ? _tid.slice(0, 8) : '';
      if (_initThreadId) {
        initThreadCookiePref(_initThreadId);
        injectCookieCheckSwitch(_initThreadId);
      }
    }
    injectImageViewerButton();                                    //阅图模式入口
    injectThreadTombstoneMark();                                  //回收站墓碑标识
    enablePaginationDuplication(!!cfg.enablePaginationDuplication); //页码栏拓展为 7 个;开关仅控制是否添加页首页码
    if (cfg.disableWatermark)            disableWatermark();        //关闭图片水印
    if (cfg.updatePreviewCookie)         updatePreviewCookieId();   //预览真实饼干
    if (cfg.hideEmptyTitleEmail) {hideEmptyTitleAndEmail();         //隐藏无名氏/无标题/回复/发串/版规
      Utils.collapse($('.h-forum-header'), '『版规』');
      Utils.collapse($('form[action="/Home/Forum/doReplyThread.html"]'), '『回复』');
      Utils.collapse($('form[action="/Home/Forum/doPostThread.html"]'), '『发串』');
      }                                                              //折叠版规-发串-回复
    //if (cfg.updateReplyNumbers)          updateReplyNumbers();     //添加回复编号
    if (cfg.enableSeamlessPaging)        initSeamlessPaging();       //自动-手动无缝翻页
    bindThreadHistoryLiveSync();
    bindPostHistoryLiveSync();
    if (cfg.enableUpdateCheck) {
      initializeUpdateReminderUI();                                   //检查更新UI状态初始化
      scheduleDailyUpdateCheck();                                     //每日检查更新
    } else {
      updateSettingsButtonBadge(getDefaultUpdateCheckState());
      clearFooterUpdateHighlight();
      if (window.__xdexUpdateCheckTimer) {
        clearTimeout(window.__xdexUpdateCheckTimer);
        window.__xdexUpdateCheckTimer = 0;
      }
    }
    // 顶栏刷新:ready 同步阶段再兜底一次(幂等),不必等 batch2 50ms 队列
    try {
      if (typeof installOverrideTopImageClickEarly === 'function') installOverrideTopImageClickEarly();
      else if (typeof overrideTopImageClick === 'function') overrideTopImageClick();
    } catch (e) {}
    interceptReplyForm();                                            //拦截回复中间页
    enhancePostFormLayout();                                         //发帖UI调整
    autoSelectReportReason();                                        //举报理由默认选择“其他”
    if (cfg.toggleSidebar)               toggleSidebar();            //侧边栏收起功能
    renderFavoriteThreadsMenu();                                      //常用串
    xdexExportInit();                                                //串内容导出(扩展后台任务)
    if (isThreadHistoryPageActive()) {
      recordCurrentThreadHistory(0, { reason: 'initial-load', countVisit: true });
      installThreadHistoryReactivationTracking(true);
    } else {
      installThreadHistoryReactivationTracking(false);
    }
    startupPerfDebug.mark('document.ready.syncSetup.end', startupPerfDebug.summarizeRoot(document));
    // 保存原始函数
    const _initContent = window.initContent;
    // 将X岛原版https://www.nmbxd1.com/Public/Js/h.desktop.js中作用于引用的initContent函数支持我们新增的非标准引用格式
    window.initContent = function(root) {
        // 先执行原始逻辑
        if (typeof _initContent === 'function') {
            _initContent(root);
        }
        // 再执行扩展逻辑
        if (root) initExtendedContent(root);
    };
    deferStartupSteps([
      { label: 'startup.batch1.enableHDImageAndLayoutFix', run: () => { if (cfg.enableHDImageAndLayoutFix) enableHDImageAndLayoutFix(document); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch1.enableHDImage', run: () => { if (cfg.enableHDImageAndLayoutFix) enableHDImage(document); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch1.applyImageHideMode', run: () => { if (cfg.enableImageHideMode) applyImageHideMode(cfg.applyImageHideMode || 'default', document); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch1.highlightPO', run: () => highlightPO(), meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch1.enablePostExpand', run: () => { enablePostExpand(document); try { bindBoardThreadRefreshButtons(document); } catch (e) {} }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch1.refreshFilterDisplay', run: () => refreshFilterDisplay(cfg), meta: () => startupPerfDebug.summarizeRoot(document) }
    ], 0, 'batch1-layout-filter-image');
    deferStartupSteps([
      { label: 'startup.batch2.ExternalImagePreview.init', run: () => { if (cfg.enableExternalImagePreview) ExternalImagePreview.init(); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.runLinkBlank', run: () => { if (cfg.enableLinkBlank) runLinkBlank(); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.runAutoUrlLinkify', run: () => { if (cfg.enableAutoUrlLinkify) runAutoUrlLinkify(); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.enableQuotePreview', run: () => { if (cfg.enableQuotePreview) enableQuotePreview(); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.replaceRightSidebar', run: () => replaceRightSidebar(), meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.extendQuote', run: () => { if (cfg.extendQuote) extendQuote(); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.kaomojiEnhancer', run: () => kaomojiEnhancer(), meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.initExtendedContent', run: () => initExtendedContent(), meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.searchServiceBy4sY', run: () => searchServiceBy4sY(), meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.monitorRefView', run: () => monitorRefView(), meta: () => startupPerfDebug.summarizeRoot(document) },
      // overrideTopImageClick 已提前到 early/ready 同步阶段;这里仅作幂等兜底
      { label: 'startup.batch2.overrideTopImageClick', run: () => overrideTopImageClick(), meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.enhanceIsland', run: () => enhanceIsland({ enablePreview: true, enableDraft: true, enableAutoTitle: true, enableRelativeTime: true, timeDisplayMode: cfg.timeDisplayMode, enableQuoteInsert: true, enablePasteImage: true }), meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.initContent(document)', run: () => initContent(document), meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.installCookieDropdownShortcutHandler', run: () => { if (cfg.enableCookieSwitch) installCookieDropdownShortcutHandler(); }, meta: () => startupPerfDebug.summarizeRoot(document) },
      { label: 'startup.batch2.bindCtrlEnter', run: () => document.querySelectorAll('form textarea[name="content"]').forEach(bindCtrlEnter), meta: () => startupPerfDebug.summarizeRoot(document) }
    ], 50, 'batch2-preview-content');
    // 屏蔽原站点的 initImageBox,改由 enableHDImageAndLayoutFix 负责初始化
    // window.initImageBox = function() {
    //   // 屏蔽原站点的逻辑
    //   console.debug("initImageBox 已被屏蔽,由 enableHDImageAndLayoutFix 接管");
    // };
    // 自动刷新并提示当前饼干,我不知道为什么必须写在这里。
    function autoRefreshCookiesToast() {
      try {
        if (typeof refreshCookies !== 'function') return;
        refreshCookies(() => {
          const list = getCookiesList();
          if (!list || !Object.keys(list).length) return;
          const cur = getCurrentCookie();
          const nm = cur && cur.name ? abbreviateName(cur.name) : '未知';
          if (cfg.enableAutoCookieRefreshToast) {
              toast(`自动刷新成功!当前饼干为 ${nm}`);
                }
        }, false); // ★ 不显示默认toast
      } catch (e) {
        console.warn('自动刷新饼干失败', e);
      }
    }
    // 仅在“从其它标签页切回到本标签页”时刷新
    const TAB_ACTIVE_KEY = 'xdex_active_tab';
    const tabId = Date.now() + '_' + Math.random().toString(36).slice(2);
    try {
      if (document.visibilityState === 'visible') {
        localStorage.setItem(TAB_ACTIVE_KEY, JSON.stringify({ id: tabId, t: Date.now() }));
      }
    } catch {}
    // ✅ 加上开关判断
    if (cfg.enableAutoCookieRefresh) {
      document.addEventListener('visibilitychange', () => {
        if (document.visibilityState !== 'visible') return;
          try {
            const last = JSON.parse(localStorage.getItem(TAB_ACTIVE_KEY) || 'null');
            if (last && last.id && last.id !== tabId) {
              autoRefreshCookiesToast();
            }
            localStorage.setItem(TAB_ACTIVE_KEY, JSON.stringify({ id: tabId, t: Date.now() }));
          } catch (e) {
            try {
              localStorage.setItem(TAB_ACTIVE_KEY, JSON.stringify({ id: tabId, t: Date.now() }));
            } catch {}
          }
      }, { passive: true });
    }
    startupPerfDebug.mark('document.ready.end', startupPerfDebug.summarizeRoot(document));
    // 全局:在运行时立即应用“全部展开 / 全部收起”模式
    window.applyPostExpandAllMode = function(enable) {
      const threads = Array.from(document.querySelectorAll('.h-threads-item-index'));
      document.documentElement.classList.toggle('xdex-post-expand-all', !!enable);
      if (!threads.length) return;
      if (enable) {
        // 全部展开:只切根节点 class;按钮文字分帧更新,避免同步逐串改 expanded 堵塞版块页。
        threads.forEach(item => item.classList.remove('xdex-post-expand-collapsed'));
        let idx = 0;
        const updateButtons = () => {
          const end = Math.min(idx + 12, threads.length);
          for (; idx < end; idx++) {
            const btn = threads[idx].querySelector('.h-threads-info .js-toggle-mode');
            if (btn) setPostExpandToggleVisual(btn, true);
          }
          if (idx < threads.length) requestAnimationFrame(updateButtons);
        };
        requestAnimationFrame(updateButtons);
      } else {
        // 全部收起:取消根节点 class,并分批收起仍处于单串 expanded 的项目。
        let delayIdx = 0;
        threads.forEach(item => {
          item.classList.remove('xdex-post-expand-collapsed');
          const shouldCollapse = item.classList.contains('expanded');
          const d = delayIdx++ * 45; // 45ms 间隔(经验值)
          setTimeout(() => {
            const btn = item.querySelector('.h-threads-info .js-toggle-mode');
            if (btn) setPostExpandToggleVisual(btn, false);
            if (!shouldCollapse) return;
            try { collapseWithoutShift(item); } catch (err) { // 防守
              try { item.classList.remove('expanded'); } catch(e){}
            }
          }, d);
        });
      }
    };
  });
  }
  scheduleXDexStartup();
})(jQuery);