☰

X岛-EX

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램을 먼저 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);