PTE Pixiv→Eagle 标签管理

一键导入 Pixiv 图片/动图到 Eagle;支持详情/列表/勾选三种模式;实时进度/ETA/可取消;面板可拖拽并记忆位置;本地或 Eagle 模式切换;作者文件夹自动归档。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         PTE  Pixiv→Eagle 标签管理
// @name:en      PTE  Pixiv→Eagle Tag Manager
// @author       Mliechoy
// @version      1.1
// @description        一键导入 Pixiv 图片/动图到 Eagle;支持详情/列表/勾选三种模式;实时进度/ETA/可取消;面板可拖拽并记忆位置;本地或 Eagle 模式切换;作者文件夹自动归档。
// @description:en     One-click import Pixiv to Eagle (ugoira→GIF); detail/list/selected modes; progress & ETA; cancel; draggable panel with position memory; local only.
// @description:ja     Pixiv を Eagle にワンクリックで取り込み(ugoira→GIF 含む);詳細/一覧/選択の取り込み;進捗・ETA・キャンセル;ドラッグ可能&位置記憶のパネル;ローカル通信。
// @description:zh-TW  一鍵匯入 Pixiv 至 Eagle(含 ugoira→GIF);支援詳情/列表/勾選;進度列/ETA/可取消;面板可拖曳並記憶位置;僅本機通訊。
// @match        https://www.pixiv.net/*
// @grant        GM_xmlhttpRequest
// @grant        GM_download
// @connect      localhost
// @connect      127.0.0.1
// @connect      i.pximg.net
// @connect      cdn.jsdelivr.net
// @run-at       document-idle
// @license      MIT
// @homepage     https://github.com/Mlietial/Save-Pixiv-picture-to-eagle
// @require      https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/gif.min.js
// @require      https://unpkg.com/pinyin-pro
// @namespace https://pte-script.example
// ==/UserScript==

(function () {
  'use strict';

  /******************** 常量 & 工具 ********************/
  const BIG_GIF_LIMIT = 40 * 1024 * 1024; // 约 40MB:ugoira→GIF 体积超过此值时优先切换为本地模式
  const EAGLE = { base: 'http://localhost:41595', api: { add: '/api/item/addFromURLs', list: '/api/folder/list', create: '/api/folder/create', update: '/api/folder/update' } };
  const QWEN_API = { endpoints: ['http://localhost:11434/v1/chat/completions'], model: 'qwen2.5:14b', timeout: 15000 };
  const LSKEY = 'pxeMini';
  const LS = {
    get(k, d) {
      try {
        const val = localStorage.getItem(LSKEY + ':' + k);
        if (val === null) return d;
        // JSON解析
        try {
          return JSON.parse(val);
        } catch {
          // 失败返回原值
          return val;
        }
      } catch {
        return d;
      }
    },
    set(k, v) { try { localStorage.setItem(LSKEY + ':' + k, typeof v === 'string' ? v : JSON.stringify(v)); } catch { } }
  };
  const sanitize = s => (s || '').replace(/[\r\n]+/g, ' ').replace(/[\/\\:*?"<>|]/g, '_').trim();
  const lower = s => (s || '').toLowerCase();

  // 拼音匹配函数
  const pinyinMatch = (text, query) => {
    if (!query) return true;
    const queryLower = query.toLowerCase();

    // 直接匹配中文
    if (text.toLowerCase().includes(queryLower)) {
      return true;
    }

    // pinyin-pro 库匹配
    try {
      if (typeof window !== 'undefined') {
        let pinyinLib = null;
        if (window.pinyinPro && typeof window.pinyinPro.pinyin === 'function') {
          pinyinLib = window.pinyinPro;
        } else if (window.pinyin && typeof window.pinyin.pinyin === 'function') {
          pinyinLib = window.pinyin;
        }

        if (pinyinLib) {
          const pinyinArray = pinyinLib.pinyin(text, {
            toneType: 'none',
            type: 'array'
          });

          if (Array.isArray(pinyinArray) && pinyinArray.length > 0) {
            let fullPinyin = '';
            let firstLetters = '';

            for (const p of pinyinArray) {
              if (p && p.length > 0) {
                fullPinyin += p;
                firstLetters += p[0];
              }
            }

            if (fullPinyin.includes(queryLower) || firstLetters.includes(queryLower)) {
              return true;
            }
          }
        }
      }
    } catch (e) {
      // 错误时返回 false
    }

    return false;
  };

  const sleep = ms => new Promise(r => setTimeout(r, ms));

  /******************** Qwen 本地翻译函数 ********************/
  async function translateWithQwen(text, targetLang = 'zh') {
    // 缓存避免重复调用
    const cacheKey = `trans_${lower(text)}_${targetLang}`;
    const cached = LS.get(cacheKey);
    if (cached) return cached;

    try {
      const prompt = targetLang === 'zh'
        ? `将以下内容翻译成中文,只回复翻译结果,不要加任何前缀或解释:${text}`
        : `Translate the following text to ${targetLang}. Only reply with the translation result, no prefix or explanation: ${text}`;

      // 向 Ollama 请求
      return new Promise((resolve) => {
        GM_xmlhttpRequest({
          method: 'POST',
          url: QWEN_API.endpoints[0],
          headers: { 'Content-Type': 'application/json' },
          data: JSON.stringify({
            model: QWEN_API.model,
            messages: [{ role: 'user', content: prompt }],
            max_tokens: 200,
            temperature: 0.3,
            stream: false
          }),
          timeout: QWEN_API.timeout,
          onload: (res) => {
            try {
              if (res.status === 200) {
                const data = JSON.parse(res.responseText);
                const result = data.choices?.[0]?.message?.content?.trim() || '';
                if (result && result.length > 0) {
                  LS.set(cacheKey, result);
                  resolve(result);
                  return;
                }
              }
              resolve(text); // 失败时返回原文
            } catch (e) {
              console.warn('[Qwen 翻译] 响应解析失败:', e);
              resolve(text);
            }
          },
          onerror: () => {
            console.warn(`[Qwen 翻译] 连接失败,请确保 Ollama 正在运行:ollama run qwen2.5:14b`);
            resolve(text);
          },
          ontimeout: () => {
            console.warn(`[Qwen 翻译] 请求超时(${QWEN_API.timeout}ms),模型响应较慢`);
            resolve(text);
          }
        });
      });
    } catch (e) {
      console.warn('[Qwen 翻译] 错误:', e);
      return text;
    }
  }

  /******************** 运行参数 ********************/
  const CFG = { filters: { bookmarkMin: 0, excludeTags: LS.get('excludeTags', ''), pageRange: '' }, ui: { x: 24, y: 24, margin: 16 }, feature: { useUploadAsAddDate: !!LS.get('useUploadAsAddDate', false), translateTags: !!LS.get('translateTags', false) }, mode: LS.get('mode', 'eagle') };

  /******************** Eagle API ********************/
  function xhr({ url, method = 'GET', data = null, timeout = 30000, raw = false }) {
    // 统一请求处理
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        url,
        method,
        // raw=true 时 data 为字符串
        data: data ? (raw ? data : JSON.stringify(data)) : null,
        headers: { 'Content-Type': 'application/json' },
        timeout,
        onload: (res) => { try { resolve(JSON.parse(res.responseText || '{}')); } catch { resolve({}); } },
        onerror: () => reject(new Error('Eagle连接失败')),
        ontimeout: () => reject(new Error('Eagle请求超时'))
      });
    });
  }

  async function listFolders() { const r = await xhr({ url: EAGLE.base + EAGLE.api.list }); return (r && r.data) || r.folders || []; }
  async function createFolder(name, parentId) {
    const payload = parentId ? { folderName: name, parent: parentId } : { folderName: name, isRoot: true };
    const r = await xhr({ url: EAGLE.base + EAGLE.api.create, method: 'POST', data: payload });
    return r?.data?.id || r?.id || r?.folderId;
  }
  async function updateFolderDesc(id, desc) {
    await xhr({ url: EAGLE.base + EAGLE.api.update, method: 'POST', data: { folderId: id, newDescription: desc, description: desc } });
  }
  function flattenFolders(tree) {
    const out = []; const st = [...(Array.isArray(tree) ? tree : [tree])].filter(Boolean);
    while (st.length) { const f = st.shift(); out.push(f); if (f.children && f.children.length) st.push(...f.children); }
    return out;
  }
  async function addToEagle(items, folderId) {
    // 体积限制由浏览器处理
    const payload = { items, folderId };
    const json = JSON.stringify(payload);
    return await xhr({ url: EAGLE.base + EAGLE.api.add, method: 'POST', data: json, raw: true });
  }
  /******************** Toast 提示 ********************/
  function showToast(message, duration = 3000) {
    const id = 'pte-toast-' + Date.now();
    const toast = document.createElement('div');
    toast.id = id;
    Object.assign(toast.style, {
      position: 'fixed',
      bottom: '24px',
      left: '50%',
      transform: 'translateX(-50%)',
      background: 'rgba(0, 0, 0, 0.85)',
      color: '#fff',
      padding: '12px 20px',
      borderRadius: '8px',
      fontSize: '14px',
      zIndex: 2147483648,
      maxWidth: '80vw',
      wordBreak: 'break-word',
      whiteSpace: 'pre-wrap',
      lineHeight: '1.5',
      animation: 'pte-toast-in 0.3s ease-out',
      boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)'
    });
    toast.textContent = message;


    if (!document.getElementById('pte-toast-style')) {
      const style = document.createElement('style');
      style.id = 'pte-toast-style';
      style.textContent = `
        @keyframes pte-toast-in {
          from { opacity: 0; transform: translateX(-50%) translateY(20px); }
          to { opacity: 1; transform: translateX(-50%) translateY(0); }
        }
        @keyframes pte-toast-out {
          from { opacity: 1; transform: translateX(-50%) translateY(0); }
          to { opacity: 0; transform: translateX(-50%) translateY(20px); }
        }
      `;
      document.head.appendChild(style);
    }

    document.body.appendChild(toast);

    setTimeout(() => {
      toast.style.animation = 'pte-toast-out 0.3s ease-in forwards';
      setTimeout(() => toast.remove(), 300);
    }, duration);
  }

  /******************** 页面工具 & 统一 Fetch 包装 ********************/
  const aborters = new Set();
  function cancelInflight() { for (const a of aborters) { try { a.abort(); } catch { } } aborters.clear(); }

  // fetch 统一包装
  async function fetchUrl(url, options = {}) {
    const { type = 'json', credentials = 'include', signal = null } = options;
    const ctrl = signal ? null : new AbortController();
    const sig = signal || ctrl?.signal;
    if (ctrl) aborters.add(ctrl);
    try {
      const res = await fetch(url, { credentials, signal: sig });
      if (type === 'json') return await res.json();
      if (type === 'text') return await res.text();
      if (type === 'arrayBuffer') return await res.arrayBuffer();
      return res;
    } finally {
      if (ctrl) aborters.delete(ctrl);
    }
  }

  // 向后兼容的快捷函数
  async function getJSON(url) { return fetchUrl(url, { type: 'json' }); }
  async function getTEXT(url) { return fetchUrl(url, { type: 'text' }); }

  function isUser() { return /\/users\/\d+/.test(location.pathname); }
  function isArtwork() { return /\/artworks\/\d+/.test(location.pathname); }

  async function allIllustIds(uid) { const r = await getJSON(`https://www.pixiv.net/ajax/user/${uid}/profile/all`); const ill = r.body?.illusts ? Object.keys(r.body.illusts) : []; const man = r.body?.manga ? Object.keys(r.body.manga) : []; return [...new Set([...ill, ...man])]; }
  function ogTitle(html) { const m = html.match(/<meta[^>]+property=['"]og:title['"][^>]*content=['"]([^'"]+)['\"]/i); return m ? sanitize(m[1]) : ''; }
  async function illustInfoAndPages(id) {
    // 自动重试避免占位信息
    const tryFetch = async () => {
      const info = await getJSON(`https://www.pixiv.net/ajax/illust/${id}`);
      const pages = await getJSON(`https://www.pixiv.net/ajax/illust/${id}/pages`);
      const b = info.body || {};
      const pageUrls = (pages.body || []).map(p => p.urls?.original).filter(Boolean);
      const tagList = Array.isArray(b.tags?.tags) ? b.tags.tags : [];
      const tags = tagList.map(t => t?.tag || t?.translation?.en || t?.translation?.ja || '').filter(Boolean);
      return {
        title: sanitize(b.title || `pixiv_${id}`),
        tags, pageUrls,
        userId: b.userId,
        userName: sanitize(b.userName || b.userAccount || ''),
        illustType: b.illustType,
        bookmarkCount: b.bookmarkCount || 0,
        uploadDate: b.uploadDate
      };
    };

    let meta = await tryFetch();

    // 如遇到标签为空 / 标题为占位 / 用户信息缺失,进行最多两次补救性重试
    if ((!meta.tags?.length) || /^pixiv_\d+$/.test(meta.title) || !meta.userId || !meta.userName) {
      for (let i = 0; i < 2; i++) {
        await sleep(300 + i * 300);
        const nx = await tryFetch();
        if ((!meta.tags?.length) && nx.tags?.length) meta.tags = nx.tags;
        if (/^pixiv_\d+$/.test(meta.title) && !/^pixiv_\d+$/.test(nx.title)) meta.title = nx.title;
        if (!meta.uploadDate && nx.uploadDate) meta.uploadDate = nx.uploadDate;
        if (!meta.userId && nx.userId) meta.userId = nx.userId;
        if (!meta.userName && nx.userName) meta.userName = nx.userName;
      }
      // 仍然是占位标题时,最后尝试从网页 og:title 中兜底一次
      if (/^pixiv_\d+$/.test(meta.title)) {
        try {
          const html = await getTEXT(`https://www.pixiv.net/artworks/${id}`);
          const og = ogTitle(html);
          if (og) meta.title = og;
        } catch { }
      }
    }

    // 标签中添加作者名
    if (!meta.tags?.length) {
      meta.tags = meta.userName ? [meta.userName] : [];
    } else {
      meta.tags = Array.from(new Set([meta.userName, ...meta.tags].filter(Boolean)));
    }
    return meta;
  }

  async function ugoiraMeta(id) { return await getJSON(`https://www.pixiv.net/ajax/illust/${id}/ugoira_meta`); }
  function parseRange(str) {
    if (!str) return null; const s = str.trim(); if (!s) return null;
    const a = s.match(/^(\d+)-(\d+)$/); if (a) { const x = +a[1], y = +a[2]; if (x > 0 && y >= x) return [x, y]; }
    const b = s.match(/^(\d+)$/); if (b) { const n = +b[1]; if (n > 0) return [n, n]; } return null;
  }

  /******************** Welcome Modal ********************/
  function createWelcomeModal(updatedAtTs) {
    if (document.getElementById('pteWelcome')) return;

    // 从脚本 metadata 动态读取版本号
    let PTE_VER = '1.1';
    let PTE_UPDATED_DATE = '2025-11-20';

    try {
      if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.version) {
        PTE_VER = GM_info.script.version;
      } else if (document.currentScript && document.currentScript.textContent) {
        const match = /@version\s+([0-9.]+)/i.exec(document.currentScript.textContent);
        if (match) PTE_VER = match[1];
      }

      if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.installed) {
        PTE_UPDATED_DATE = new Date(GM_info.script.installed).toISOString().split('T')[0];
      }
    } catch (e) {
      // 使用默认值
    }

    var mask = document.createElement('div');
    mask.id = 'pteWelcome';
    Object.assign(mask.style, {
      position: 'fixed', inset: '0',
      background: 'rgba(0,0,0,.35)',
      backdropFilter: 'blur(2px)',
      zIndex: 2147483647,
      display: 'flex', alignItems: 'center', justifyContent: 'center'
    });
    var box = document.createElement('div');
    Object.assign(box.style, {
      width: 'min(560px,92vw)',
      borderRadius: '16px',
      background: '#fff',
      boxShadow: '0 12px 40px rgba(0,0,0,.18)',
      padding: '16px 18px',
      fontSize: '13px',
      color: '#444',
      lineHeight: '1.6',
      maxHeight: '80vh', overflow: 'auto'
    });
    var timeStr = PTE_UPDATED_DATE;
    box.innerHTML = ''
      + '<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;">'
      + '<div style="font-size:18px;font-weight:700;color:#1f6fff;">PTE 已更新 ✅</div>'
      + '<span style="margin-left:auto;color:#999;font-size:12px">v' + PTE_VER + '</span>'
      + '</div>'
      + '<div style="color:#999;font-size:12px;margin-bottom:8px;">更新时间:' + timeStr + ' | 版本号:v' + PTE_VER + '</div>'
      + '<div>'
      + '<p>右上角工具条:<b style="color:#409eff">E(蓝)</b> = Eagle 模式,<b style="color:#f1a72e">D(橙)</b> = 本地模式。</p>'
      + '<p>详情页六键:<code>此作</code> / <code>本页</code> / <code>仅勾选</code> / <code>全选</code> / <code>全不选</code> / <code>下一页</code>。</p><p>顶部工具条新增并固定"🕒 投稿时间→添加日期"开关(点击切换;关闭时灰度显示)。</p>'
      + '<p>第二页:🔁 反选 · 📁 选择下载目录(左下) · 📜 公告 · ⬅️ 上一页(右下)。</p>'
      + '<p><b style="color:#ff4d4f">大动图说明:</b> 当 ugoira→GIF 体积过大(约 &gt;40MB)时,脚本会自动从 Eagle 模式切换为"保存到本地"模式,并保存到下载目录下的 <code>Pixiv/作者名_作者ID/作品ID.gif</code>,以避免浏览器 / 油猴在导入 Eagle 时因消息过长而卡住。</p>'
      + '<p style="color:#666">小技巧:点击绿灯检查 Eagle;点"➖"可缩小为悬浮圆点。</p>'
      + '<p style="margin-top:6px"><b>没看到弹窗/工具条?</b> 如果脚本已启动但首次没看到,UI 可能在浏览器窗口右侧;请尝试将浏览器窗口<b>拉宽</b>即可看见。</p>'
      + '<p><b>连续多选:</b> 在列表/缩略图页,先点击左侧的勾选框选中一项,然后按住 <kbd>Shift</kbd> 再点击另一项,<b>两者之间的范围</b>会被一次性选中。</p>'
      + '</div>'
      + '<div style="display:flex;gap:10px;margin-top:14px;justify-content:flex-end;">'
      + '<button id="pxeWelcomeOk" style="padding:6px 14px;border:none;border-radius:8px;background:#409eff;color:#fff;cursor:pointer;font-weight:600">我知道了</button>'
      + '</div>';
    mask.appendChild(box);
    document.body.appendChild(mask);
    mask.addEventListener('click', function (e) { if (e.target === mask) mask.remove(); });
    var ok = box.querySelector('#pxeWelcomeOk');
    if (ok) ok.addEventListener('click', function () { mask.remove(); });
  }

  /******************** 操作历史记录函数 ********************/
  function addOperationLog(action, details) {
    try {
      let logs = LS.get('operationLogs', []);
      if (typeof logs === 'string') {
        try { logs = JSON.parse(logs); } catch { logs = []; }
      }
      if (!Array.isArray(logs)) logs = [];

      const now = new Date();
      const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
      const logEntry = {
        action: action,
        details: details,
        timestamp: timeStr,
        date: now.toISOString().split('T')[0]
      };
      logs.push(logEntry);

      // Debug 日志:操作记录
      debugLog('LOG', '添加操作日志', { action, details, timestamp: timeStr });

      // 在localStorage中保持最近20条记录(防止数据无限增长)
      if (logs.length > 20) {
        logs = logs.slice(-20);
      }

      LS.set('operationLogs', logs);

      // 更新历史显示
      const historyDiv = document.getElementById('pteOperationHistory');
      if (historyDiv) {
        updateOperationHistory();
      }
    } catch(e) {
      // silent
    }
  }

  // 全局变量 - 标签管理(最简单直接的方式)
  var savedTags = (function() {
    let data = LS.get('tagTranslations', {});
    if (typeof data === 'string') {
      try { data = JSON.parse(data); } catch { data = {}; }
    }
    return (data && typeof data === 'object') ? data : {};
  })();

  function updateOperationHistory() {
    try {
      const logs = LS.get('operationLogs', []);
      if (typeof logs === 'string') {
        try { var parsedLogs = JSON.parse(logs); } catch { parsedLogs = []; }
      } else {
        parsedLogs = Array.isArray(logs) ? logs : [];
      }

      const historyDiv = document.getElementById('pteOperationHistory');
      if (!historyDiv) return;

      if (parsedLogs.length === 0) {
        historyDiv.innerHTML = '<div style="color:#999;">暂无操作记录</div>';
        return;
      }

      let html = '';
      // 显示最近的2条记录
      parsedLogs.slice(-2).reverse().forEach(log => {
        html += `<div style="font-size:11px;color:#333;">${log.action}${log.details ? ' - ' + log.details : ''} <span style="color:#999;margin-left:8px;">${log.timestamp}</span></div>`;
      });

      historyDiv.innerHTML = html;
    } catch(e) {
      // silent
    }
  }

  /******************** 统一标签管理弹窗 ********************/

  // Debug 日志函数
  const debugLog = (category, message, data = null) => {
    const settings = LS.get('tagManagerSettings', { debugMode: false });
    if (settings.debugMode) {
      const timestamp = new Date().toLocaleTimeString('zh-CN');
      console.log(`[${timestamp}] [PTE-${category}] ${message}`, data || '');
    }
  };

  async function createTagManagerModal() {
    if (document.getElementById('pteTagManager')) return;

    const mask = document.createElement('div');
    mask.id = 'pteTagManager';
    Object.assign(mask.style, {
      position: 'fixed', inset: '0',
      background: 'transparent',
      backdropFilter: 'none',
      zIndex: 2147483647,
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: '20px'
    });

    const box = document.createElement('div');
    Object.assign(box.style, {
      width: 'min(1200px,95vw)',
      borderRadius: '16px',
      background: '#fff',
      boxShadow: '0 12px 40px rgba(0,0,0,.18)',
      padding: '20px',
      fontSize: '13px',
      color: '#444',
      lineHeight: '1.6',
      maxHeight: '90vh',
      overflow: 'auto'
    });

    // 直接从 localStorage 读取最新的排除标签
    const excludedTags = LS.get('excludeTags', '') || CFG.filters.excludeTags || '';

    box.innerHTML = `
      <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;border-bottom:2px solid #409eff;padding-bottom:12px;">
        <div style="font-size:18px;font-weight:700;color:#1f6fff;">🏷️ 标签管理中心</div>
        <span style="margin-left:auto;color:#666;font-size:12px;">已保存翻译: ${Object.keys(savedTags).length} | 已排除: ${excludedTags.split(',').filter(Boolean).length}</span>
      </div>

      <!-- 操作历史 -->
      <div style="background:#e8f4f8;border-left:4px solid #00bcd4;padding:12px;border-radius:4px;margin-bottom:8px;">
        <div style="font-weight:600;color:#00695c;margin-bottom:4px;font-size:12px;">📋 最近操作</div>
        <div id="pteOperationHistory" style="font-size:11px;color:#00695c;max-height:50px;overflow-y:auto;line-height:1.6;">
          <div style="color:#999;">暂无操作记录</div>
        </div>
      </div>

      <!-- 三列布局 -->
      <div style="display:grid;grid-template-columns:1fr 1.5fr 1fr;gap:12px;height:400px;">

        <!-- 左列:排除标签 -->
        <div style="display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;padding:12px;background:#fafafa;">
          <div style="font-weight:600;color:#f57c00;margin-bottom:8px;display:flex;align-items:center;gap:6px;justify-content:space-between;">
            <div style="display:flex;align-items:center;gap:6px;">
              <span>🚫 排除标签</span>
              <span style="font-size:11px;color:#999;font-weight:400;" id="pteExcludedCount">(0)</span>
            </div>
            <div style="display:flex;gap:4px;">
              <input id="pteManualExcludeInput" type="text" placeholder="输入标签" style="width:80px;padding:4px 6px;border:1px solid #d9d9d9;border-radius:3px;font-size:10px;box-sizing:border-box;" />
              <button id="pteManualExcludeAdd" style="width:24px;height:24px;border:none;border-radius:3px;background:#409eff;color:#fff;cursor:pointer;font-size:10px;font-weight:600;flex-shrink:0;display:flex;align-items:center;justify-content:center;">+</button>
              <div style="position:relative;">
                <button id="pteExcludeSort" style="width:24px;height:24px;border:none;border-radius:3px;background:#409eff;color:#fff;cursor:pointer;font-size:10px;font-weight:600;flex-shrink:0;display:flex;align-items:center;justify-content:center;" title="点击切换排序方式">↑</button>
                <div id="pteSortMenu" style="display:none;position:absolute;top:100%;right:-8px;margin-top:2px;background:#fff;border:1px solid #d9d9d9;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.15);z-index:10000;width:fit-content;">
                  <div data-sort="alpha-asc" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;border-bottom:1px solid #f0f0f0;white-space:nowrap;" title="字母升序">A→Z</div>
                  <div data-sort="alpha-desc" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;border-bottom:1px solid #f0f0f0;white-space:nowrap;" title="字母降序">Z→A</div>
                  <div data-sort="time-new" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;border-bottom:1px solid #f0f0f0;white-space:nowrap;" title="最新添加优先">新→旧</div>
                  <div data-sort="time-old" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;white-space:nowrap;" title="最早添加优先">旧→新</div>
                </div>
              </div>
            </div>
          </div>
          <div id="pteExcludeList" style="flex:0 0 auto;overflow-y:auto;max-height:233px;min-height:250px;margin-bottom:6px;padding:8px;border:1px solid #d9d9d9;border-radius:6px;background:#fff;display:flex;flex-direction:column;gap:6px;"></div>
          <div style="display:flex;gap:6px;margin-bottom:4px;">
            <button id="pteExcludeImport" style="flex:1;padding:10px;border:none;border-radius:6px;background:#409eff;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">📥 导入</button>
            <button id="pteExcludeExport" style="flex:1;padding:10px;border:none;border-radius:6px;background:#67c23a;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">📤 导出</button>
          </div>
          <div style="display:flex;gap:6px;margin-bottom:0px;">
            <button id="pteExcludeSave" style="flex:1;padding:10px;border:none;border-radius:6px;background:#ff9800;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">💾 保存</button>
            <button id="pteExcludeReset" style="flex:1;padding:10px;border:1px solid #f56c6c;border-radius:6px;background:#fff;color:#f56c6c;cursor:pointer;font-weight:600;font-size:12px;">🗑️ 清空</button>
          </div>
        </div>

        <!-- 中列:翻译区域 -->
        <div style="display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;padding:12px;background:#fafafa;min-height:0;">
          <!-- 上半部分:输入和结果 -->
          <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;height:calc(100% - 80px);margin-bottom:6px;">
            <!-- 输入 -->
            <div style="display:flex;flex-direction:column;min-height:0;overflow:hidden;height:100%;">
              <div style="font-weight:600;color:#1976d2;margin-bottom:6px;font-size:12px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;">
                <span>📝 待翻译</span>
                <div style="display:flex;gap:4px;align-items:center;">
                  <input id="pteManualTransInput" type="text" placeholder="输入标签" style="width:80px;padding:4px 6px;border:1px solid #d9d9d9;border-radius:3px;font-size:10px;box-sizing:border-box;" />
                  <button id="pteManualTransAdd" style="width:24px;height:24px;border:none;border-radius:3px;background:#409eff;color:#fff;cursor:pointer;font-size:10px;font-weight:600;flex-shrink:0;display:flex;align-items:center;justify-content:center;">+</button>
                  <button id="pteQuickExclude" style="width:36px;height:24px;border:none;border-radius:3px;background:#f56c6c;color:#fff;cursor:pointer;font-size:10px;font-weight:600;flex-shrink:0;display:flex;align-items:center;justify-content:center;" title="将待翻译中的标签移到排除列表">排除</button>
                </div>
              </div>
              <div id="pteTransInputList" style="flex:1;overflow-y:auto;padding:8px;border:1px solid #d9d9d9;border-radius:6px;background:#f9f9f9;display:flex;flex-direction:column;gap:6px;min-height:0;">
                <div style="color:#999;text-align:center;padding:30px 10px;font-size:12px;">暂无待翻译标签</div>
              </div>
              <textarea id="pteTransInput" style="display:none;flex:1;padding:8px;border:1px solid #d9d9d9;border-radius:6px;font-family:monospace;font-size:11px;resize:none;box-sizing:border-box;max-height:400px;overflow-y:auto;" placeholder="每行一个"></textarea>
            </div>
            <!-- 结果 -->
            <div style="display:flex;flex-direction:column;min-height:0;overflow:hidden;height:100%;">
              <div style="font-weight:600;color:#1976d2;margin-bottom:6px;font-size:12px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;">
                <span>✏️ 翻译结果</span>
                <button id="pteClearTransResult" title="标签管理设置" style="width:24px;height:24px;border:none;border-radius:3px;background:#409eff;color:#fff;cursor:pointer;font-size:12px;font-weight:600;flex-shrink:0;display:flex;align-items:center;justify-content:center;">⚙️</button>
              </div>
              <div id="pteTransResult" style="flex:1;border:1px solid #d9d9d9;border-radius:6px;padding:8px;background:#f9f9f9;overflow-y:auto;font-size:11px;min-height:0;">
                <div style="color:#999;text-align:center;padding:30px 10px;">等待翻译...</div>
              </div>
            </div>
          </div>

          <!-- 操作按钮 -->
          <div style="display:flex;flex-direction:column;gap:4px;margin-top:3px;">
            <div style="display:flex;gap:6px;">
              <button id="pteExtractTags" style="flex:1;padding:10px;border:none;border-radius:6px;background:#ff9800;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">📋 提取标签</button>
              <button id="pteTranslateAll" style="flex:1;padding:10px;border:none;border-radius:6px;background:#409eff;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">🚀 翻译</button>
            </div>
            <div style="display:flex;gap:6px;">
              <button id="pteListExport" style="flex:0.44;padding:10px;border:none;border-radius:6px;background:#67c23a;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">导出列表</button>
              <button id="pteListImport" style="flex:0.44;padding:10px;border:none;border-radius:6px;background:#409eff;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">导入列表</button>
              <button id="pteSaveAll" style="flex:1;padding:10px;border:none;border-radius:6px;background:#67c23a;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">💾 保存全部</button>
            </div>
          </div>
        </div>

        <!-- 右列:已保存和工具 -->
        <div style="display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;padding:12px;background:#fafafa;flex:0 0 auto;">
          <div style="font-weight:600;color:#388e3c;margin-bottom:8px;display:flex;align-items:center;gap:6px;justify-content:space-between;">
            <div style="display:flex;align-items:center;gap:6px;">
              <span>📌 已保存翻译</span>
              <span style="font-size:11px;color:#999;font-weight:400;" id="pteSavedCount">(0)</span>
            </div>
            <div style="display:flex;gap:4px;align-items:center;">
              <input id="pteSavedSearch" type="text" placeholder="搜索翻译" style="width:80px;padding:4px 6px;border:1px solid #d9d9d9;border-radius:3px;font-size:10px;box-sizing:border-box;" />
              <div style="position:relative;">
                <button id="pteSavedSort" style="width:24px;height:24px;border:none;border-radius:3px;background:#409eff;color:#fff;cursor:pointer;font-size:10px;font-weight:600;flex-shrink:0;display:flex;align-items:center;justify-content:center;" title="点击切换排序方式">↑</button>
                <div id="pteSavedSortMenu" style="display:none;position:absolute;top:100%;right:-8px;margin-top:2px;background:#fff;border:1px solid #d9d9d9;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.15);z-index:10000;width:fit-content;">
                  <div data-sort="tag-asc" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;border-bottom:1px solid #f0f0f0;white-space:nowrap;" title="原始标签升序">标A→Z</div>
                  <div data-sort="tag-desc" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;border-bottom:1px solid #f0f0f0;white-space:nowrap;" title="原始标签降序">标Z→A</div>
                  <div data-sort="trans-asc" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;border-bottom:1px solid #f0f0f0;white-space:nowrap;" title="翻译升序">译A→Z</div>
                  <div data-sort="trans-desc" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;border-bottom:1px solid #f0f0f0;white-space:nowrap;" title="翻译降序">译Z→A</div>
                  <div data-sort="time-new" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;border-bottom:1px solid #f0f0f0;white-space:nowrap;" title="最新添加优先">新→旧</div>
                  <div data-sort="time-old" style="padding:8px 12px;cursor:pointer;font-size:11px;color:#333;white-space:nowrap;" title="最早添加优先">旧→新</div>
                </div>
              </div>
            </div>
          </div>
          <div id="pteSavedList" style="flex:0 0 auto;overflow-y:auto;max-height:233px;min-height:250px;margin-bottom:8px;padding:8px;border:1px solid #d9d9d9;border-radius:6px;background:#fff;display:flex;flex-direction:column;gap:6px;">
          </div>
          <div style="display:flex;gap:6px;flex-direction:column;margin-bottom:0px;">
            <div style="display:flex;gap:6px;">
              <button id="pteSavedImport" style="flex:1;padding:10px;border:none;border-radius:6px;background:#409eff;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">📥 导入</button>
              <button id="pteSavedExport" style="flex:1;padding:10px;border:none;border-radius:6px;background:#67c23a;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">📤 导出</button>
            </div>
            <div style="display:flex;gap:6px;">
              <button id="pteSavedSave" style="flex:1;padding:10px;border:none;border-radius:6px;background:#ff9800;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">💾 保存</button>
              <button id="pteSavedReset" style="flex:1;padding:10px;border:1px solid #f56c6c;border-radius:6px;background:#fff;color:#f56c6c;cursor:pointer;font-weight:600;font-size:12px;">🗑️ 清空</button>
            </div>
          </div>
        </div>
      </div>

      <!-- 隐藏的文件输入框 -->
      <input id="pteFileImportInput" type="file" style="display:none;" accept=".txt,.csv">

      <!-- 导入选择窗口 -->
      <div id="pteImportDialog" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border:2px solid #409eff;border-radius:8px;padding:20px;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,0.2);min-width:400px;max-width:600px;">
        <div style="font-weight:600;color:#1f6fff;margin-bottom:16px;font-size:14px;">📥 导入数据</div>
        <textarea id="pteImportTextarea" placeholder="在此粘贴导入内容..." style="width:100%;height:200px;padding:10px;border:1px solid #d9d9d9;border-radius:6px;font-family:monospace;font-size:12px;resize:none;box-sizing:border-box;"></textarea>
        <div style="display:flex;gap:10px;margin-top:16px;justify-content:flex-end;">
          <button id="pteImportCancel" style="padding:8px 16px;border:1px solid #d9d9d9;border-radius:6px;background:#f5f5f5;color:#666;cursor:pointer;font-weight:600;font-size:12px;">取消</button>
          <button id="pteImportConfirm" style="padding:8px 16px;border:none;border-radius:6px;background:#409eff;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">导入</button>
        </div>
      </div>

      <!-- 导入对话框背景遮罩 -->
      <div id="pteImportMask" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:9998;"></div>

      <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:15px;padding-top:4px;">
        <button id="pteManagerClose" style="padding:8px 16px;border:1px solid #d9d9d9;border-radius:8px;background:#fff;color:#666;cursor:pointer;font-weight:600;">关闭</button>
      </div>
    `;

    mask.appendChild(box);
    document.body.appendChild(mask);

    // 初始化"保存全部"按钮状态(默认禁用)
    const saveAllBtn = box.querySelector('#pteSaveAll');
    saveAllBtn.disabled = true;
    saveAllBtn.style.opacity = '0.5';
    saveAllBtn.style.cursor = 'not-allowed';

    // 显示已保存的翻译
    const updateSavedList = () => {
      const savedListEl = box.querySelector('#pteSavedList');
      const countEl = box.querySelector('#pteSavedCount');
      savedListEl.innerHTML = '';

      if (Object.keys(savedTags).length === 0) {
        savedListEl.innerHTML = '<div style="color:#999;text-align:center;padding:30px 10px;font-size:12px;">暂无保存的翻译</div>';
      } else {
        // 获取排序模式
        let entries = Object.entries(savedTags).map(([original, trans]) => {
          // 兼容旧常规模式
          if (typeof trans === 'string') {
            return [original, { translation: trans, timestamp: 0 }];
          }
          return [original, trans];
        });

        // 应用排序
        const sortMode = LS.get('savedSortMode', 'tag-asc') || 'tag-asc';
        if (sortMode === 'tag-asc') {
          entries.sort((a, b) => a[0].localeCompare(b[0]));
        } else if (sortMode === 'tag-desc') {
          entries.sort((a, b) => b[0].localeCompare(a[0]));
        } else if (sortMode === 'trans-asc') {
          entries.sort((a, b) => {
            const transA = typeof a[1] === 'string' ? a[1] : a[1].translation;
            const transB = typeof b[1] === 'string' ? b[1] : b[1].translation;
            return transA.localeCompare(transB);
          });
        } else if (sortMode === 'trans-desc') {
          entries.sort((a, b) => {
            const transA = typeof a[1] === 'string' ? a[1] : a[1].translation;
            const transB = typeof b[1] === 'string' ? b[1] : b[1].translation;
            return transB.localeCompare(transA);
          });
        } else if (sortMode === 'time-new') {
          entries.sort((a, b) => (b[1].timestamp || 0) - (a[1].timestamp || 0));
        } else if (sortMode === 'time-old') {
          entries.sort((a, b) => (a[1].timestamp || 0) - (b[1].timestamp || 0));
        }

        for (const [original, transData] of entries) {
          const translation = typeof transData === 'string' ? transData : transData.translation;
          const div = document.createElement('div');
          div.style.cssText = 'padding:6px 8px;border:1px solid #d9d9d9;border-radius:4px;background:#e3f2fd;font-size:11px;display:flex;align-items:center;gap:4px;';


          const origSpan = document.createElement('span');
          origSpan.style.cssText = 'color:#1f6fff;font-weight:600;width:60px;max-width:60px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0;';
          origSpan.title = original;
          origSpan.textContent = original;
          div.appendChild(origSpan);


          const arrow = document.createElement('span');
          arrow.style.cssText = 'color:#999;flex-shrink:0;';
          arrow.textContent = '→';
          div.appendChild(arrow);

          // 编辑框
          const input = document.createElement('input');
          input.type = 'text';
          input.value = translation;
          input.className = 'pteEditTranslation';
          input.setAttribute('data-original', original);
          input.style.cssText = 'flex:1;padding:2px 4px;border:1px solid #d9d9d9;border-radius:3px;font-size:11px;min-width:40px;display:none;box-sizing:border-box;';
          div.appendChild(input);


          const transSpan = document.createElement('span');
          transSpan.style.cssText = 'color:#666;min-width:40px;max-width:120px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;word-break:break-all;margin-right:auto;';
          transSpan.textContent = translation;
          transSpan.title = translation;
          transSpan.className = 'pteTransDisplay';
          div.appendChild(transSpan);

          // 编辑按钮
          const editBtn = document.createElement('button');
          editBtn.className = 'pteEditTag';
          editBtn.setAttribute('data-tag', original);
          editBtn.style.cssText = 'padding:0 4px;border:none;border-radius:3px;background:#409eff;color:#fff;cursor:pointer;font-size:10px;flex-shrink:0;';
          editBtn.textContent = '✏️';
          editBtn.onclick = (e) => {
            e.stopPropagation();
            const isEditing = input.style.display !== 'none';
            input.style.display = isEditing ? 'none' : 'block';
            transSpan.style.display = isEditing ? 'block' : 'none';
            editBtn.textContent = isEditing ? '✏️' : '✕';
            saveBtn.style.display = isEditing ? 'none' : 'block';
            if (!isEditing) input.focus();
          };
          div.appendChild(editBtn);

          // 保存修改按钮
          const saveBtn = document.createElement('button');
          saveBtn.className = 'pteSaveEdit';
          saveBtn.setAttribute('data-tag', original);
          saveBtn.style.cssText = 'padding:0 4px;border:none;border-radius:3px;background:#67c23a;color:#fff;cursor:pointer;font-size:10px;flex-shrink:0;display:none;';
          saveBtn.textContent = '💾';
          saveBtn.onclick = (e) => {
            e.stopPropagation();
            // 检查是否在排除列表中
            if (excludeTagsSet.has(original)) {
              showToast(`❌ 此标签在排除列表中,无法保存`);
              return;
            }
            const newTranslation = input.value.trim();
            if (!newTranslation) {
              showToast('翻译不能为空');
              return;
            }
            // 保存翻译及时间戳
            savedTags[original] = { translation: newTranslation, timestamp: Date.now() };
            LS.set('tagTranslations', savedTags);
            // 从待翻译列表中删除该标签
            const lines = transInput.value.split('\n');
            const filtered = lines.filter(line => line.trim() !== original);
            transInput.value = filtered.join('\n');
            updateTransInputList();
            updateSavedList();
            // 保存后重新应用搜索过滤
            const searchInput = box.querySelector('#pteSavedSearch');
            if (searchInput && searchInput.value.trim()) {
              searchInput.dispatchEvent(new Event('input'));
            }
            updateTransResultAfterExclude();
            // 记录操作
            addOperationLog('保存翻译', `${original} → ${newTranslation}`);
            updateOperationHistory();
            showToast(`✅ 已保存修改:${original}`);
          };
          div.appendChild(saveBtn);

          // 删除按钮
          const deleteBtn = document.createElement('button');
          deleteBtn.className = 'pteDeleteTag';
          deleteBtn.setAttribute('data-tag', original);
          deleteBtn.style.cssText = 'padding:0 4px;border:none;border-radius:3px;background:#f56c6c;color:#fff;cursor:pointer;font-size:10px;flex-shrink:0;';
          deleteBtn.textContent = '✕';
          deleteBtn.onclick = (e) => {
            e.stopPropagation();
            delete savedTags[original];
            LS.set('tagTranslations', savedTags);

            // 同步到中间列:删除的翻译回到待翻译区
            const currentText = transInput.value.trim();
            const lines = currentText ? currentText.split('\n') : [];
            if (!lines.includes(original)) {
              lines.push(original);
              transInput.value = lines.join('\n');
            }

            // 更新所有相关UI
            updateSavedList();
            updateTransInputList();
            // 删除后重新应用搜索过滤
            const searchInput = box.querySelector('#pteSavedSearch');
            if (searchInput && searchInput.value.trim()) {
              searchInput.dispatchEvent(new Event('input'));
            }
            // 检查翻译结果区是否有该标签,如果有则重新显示
            const transResultItem = transResult.querySelector(`input[data-original="${original}"]`);
            if (transResultItem) {
              transResultItem.closest('div').style.display = 'flex';
            }
            updateTransResultAfterExclude();
            // 记录操作
            addOperationLog('删除翻译', original);
            updateOperationHistory();
            showToast(`✅ 已删除翻译,回到待翻译区:${original}`);
          };
          div.appendChild(deleteBtn);

          savedListEl.appendChild(div);
        }
      }
      countEl.textContent = `(${Object.keys(savedTags).length})`;
    };
    updateSavedList();

    // 排除标签
    let excludeTagsSet = new Set(
      excludedTags.split(',')
        .map(t => t.trim().replace(/^["']|["']$/g, ''))
        .filter(Boolean)
    );

    // 调试:显示排除列表的实际内容
    debugLog('EXCLUDE', '排除列表初始化', {
      rawExcludedTags: excludedTags,
      excludeTagsSet: Array.from(excludeTagsSet),
      size: excludeTagsSet.size
    });

    // 排除标签的排序和时间戳
    let excludeTagsWithTime = LS.get('excludeTagsWithTime', {});
    let excludeSortMode = LS.get('excludeSortMode', 'alpha-asc'); // 'alpha-asc', 'alpha-desc', 'time-new', 'time-old'

    // 已存子标旧常排序上費会
    let initialized = LS.get('excludeTagsTimeInitialized', false);
    if (!initialized && excludeTagsSet.size > 0) {
      const now = Date.now();
      for (const tag of excludeTagsSet) {
        if (!excludeTagsWithTime[tag]) {
          excludeTagsWithTime[tag] = now;
        }
      }
      LS.set('excludeTagsWithTime', excludeTagsWithTime);
      LS.set('excludeTagsTimeInitialized', true);
    }

    const applySorting = () => {
      let sortedTags = Array.from(excludeTagsSet);
      switch (excludeSortMode) {
        case 'alpha-asc':
          sortedTags.sort();
          break;
        case 'alpha-desc':
          sortedTags.sort().reverse();
          break;
        case 'time-new':
          sortedTags.sort((a, b) => (excludeTagsWithTime[b] || 0) - (excludeTagsWithTime[a] || 0));
          break;
        case 'time-old':
          sortedTags.sort((a, b) => (excludeTagsWithTime[a] || 0) - (excludeTagsWithTime[b] || 0));
          break;
      }
      return sortedTags;
    };

    const updateExcludeList = () => {
      const excludeListEl = box.querySelector('#pteExcludeList');
      const countEl = box.querySelector('#pteExcludedCount');
      excludeListEl.innerHTML = '';
      const sortedTags = applySorting();

      if (excludeTagsSet.size === 0) {
        excludeListEl.innerHTML = '<div style="color:#999;text-align:center;padding:30px 10px;font-size:12px;">暂无排除标签</div>';
      } else {
        for (const tag of sortedTags) {
          const div = document.createElement('div');
          div.style.cssText = 'padding:6px 8px;border:1px solid #d9d9d9;border-radius:4px;background:#ffebee;font-size:11px;display:flex;align-items:center;gap:6px;';

          const tagSpan = document.createElement('span');
          tagSpan.style.cssText = 'color:#c62828;font-weight:600;flex:1;word-break:break-all;';
          tagSpan.textContent = tag;
          div.appendChild(tagSpan);

          const deleteBtn = document.createElement('button');
          deleteBtn.style.cssText = 'padding:0 4px;border:none;border-radius:3px;background:#f56c6c;color:#fff;cursor:pointer;font-size:10px;flex-shrink:0;';
          deleteBtn.textContent = '✕';
          deleteBtn.onclick = (e) => {
            e.stopPropagation();
            excludeTagsSet.delete(tag);
            delete excludeTagsWithTime[tag];
            // 立即保存到 localStorage
            const tagsStr = Array.from(excludeTagsSet).join(',');
            LS.set('excludeTags', tagsStr);
            LS.set('excludeTagsWithTime', excludeTagsWithTime);

            // 同步到中间列:添加到待翻译区
            const currentText = transInput.value.trim();
            const lines = currentText ? currentText.split('\n') : [];
            if (!lines.includes(tag)) {
              lines.push(tag);
              transInput.value = lines.join('\n');
            }

            // 同时从已保存翻译中删除该标签
            if (savedTags[tag]) {
              delete savedTags[tag];
              try { LS.set('tagTranslations', savedTags); } catch { }
              updateSavedList();
            }
            updateExcludeList();
            updateTransInputList();
            updateTransResultAfterExclude();
            showToast(`✅ 已移除排除标签,加入待翻译:${tag}`);
          };
          div.appendChild(deleteBtn);

          excludeListEl.appendChild(div);
        }
      }
      countEl.textContent = `(${excludeTagsSet.size})`;
    };
    updateExcludeList();

    // 翻译结果实时更新(排除后)
    const updateTransResultAfterExclude = () => {
      const inputs = transResult.querySelectorAll('.pteTransEdit');
      inputs.forEach(input => {
        const tag = input.dataset.original;
        const lowerTag = lower(tag);
        const isExcluded = Array.from(excludeTagsSet).some(ex => {
          return lower(ex) === lowerTag;
        });
        // 如果已保存,保持隐藏;如果被排除,隐藏;否则显示
        if (savedTags[tag]) {
          // 已保存的始终隐藏
          input.closest('div').style.display = 'none';
        } else if (isExcluded) {
          // 被排除的隐藏
          input.closest('div').style.display = 'none';
        } else {
          // 其他的显示
          input.closest('div').style.display = 'flex';
        }
      });
    };

    // 翻译相关
    const transInput = box.querySelector('#pteTransInput');
    const transInputList = box.querySelector('#pteTransInputList');
    const transResult = box.querySelector('#pteTransResult');

    // 管理待翻译标签卡片
    const updateTransInputList = () => {
      transInputList.innerHTML = '';
      const tags = transInput.value.trim().split('\n').map(t => t.trim()).filter(Boolean);

      if (tags.length === 0) {
        transInputList.innerHTML = '<div style="color:#999;text-align:center;padding:30px 10px;font-size:12px;">暂无待翻译标签</div>';
        return;
      }

      tags.forEach(tag => {
        const div = document.createElement('div');
        div.style.cssText = 'padding:6px 8px;border:1px solid #d9d9d9;border-radius:4px;background:#e3f5ff;font-size:11px;display:flex;align-items:center;justify-content:space-between;gap:4px;flex-wrap:wrap;';

        const tagSpan = document.createElement('span');
        tagSpan.style.cssText = 'color:#1976d2;font-weight:600;flex:1;word-break:break-all;min-width:0;';
        tagSpan.textContent = tag;
        div.appendChild(tagSpan);

        // 按钮容器
        const btnContainer = document.createElement('div');
        btnContainer.style.cssText = 'display:flex;gap:2px;flex-shrink:0;';

        // Pixiv搜索按钮
        const pixivBtn = document.createElement('button');
        pixivBtn.style.cssText = 'padding:0 4px;border:none;border-radius:3px;background:#409eff;color:#fff;cursor:pointer;font-size:10px;flex-shrink:0;min-width:24px;';
        pixivBtn.textContent = 'P搜';
        pixivBtn.title = '在Pixiv搜索此标签';
        pixivBtn.onclick = (e) => {
          e.stopPropagation();
          const searchUrl = `https://www.pixiv.net/tags/${encodeURIComponent(tag)}/illustrations`;
          window.open(searchUrl, '_blank');
        };
        btnContainer.appendChild(pixivBtn);

        // 搜索按钮(支持多个搜索引擎)
        const googleBtn = document.createElement('button');
        googleBtn.style.cssText = 'padding:0 4px;border:none;border-radius:3px;background:#ffc107;color:#fff;cursor:pointer;font-size:10px;flex-shrink:0;';
        googleBtn.textContent = '🔍';
        googleBtn.onclick = (e) => {
          e.stopPropagation();
          const settings = LS.get('tagManagerSettings', { searchEngine: 'google', customEngineUrl: '' });
          const engineUrls = {
            google: `https://www.google.com/search?q=${encodeURIComponent(tag)}`,
            baidu: `https://www.baidu.com/s?wd=${encodeURIComponent(tag)}`,
            bing: `https://www.bing.com/search?q=${encodeURIComponent(tag)}`,
            'yahoo-jp': `https://search.yahoo.co.jp/search?p=${encodeURIComponent(tag)}`,
            goo: `https://search.goo.ne.jp/web.jsp?MT=${encodeURIComponent(tag)}`,
            sogou: `https://www.sogou.com/web?query=${encodeURIComponent(tag)}`
          };
          const engineNames = {
            google: 'Google',
            baidu: 'Baidu',
            bing: 'Bing',
            'yahoo-jp': 'Yahoo Japan',
            goo: 'Goo',
            sogou: 'Sogou',
            custom: '自定义'
          };

          let searchUrl;
          if (settings.searchEngine === 'custom' && settings.customEngineUrl) {
            searchUrl = settings.customEngineUrl.replace('{tag}', encodeURIComponent(tag));
          } else {
            searchUrl = engineUrls[settings.searchEngine] || engineUrls.google;
          }

          googleBtn.title = `在 ${engineNames[settings.searchEngine]} 中搜索此标签`;
          debugLog('SEARCH', '用户选择搜索', { tag, engine: settings.searchEngine });
          window.open(searchUrl, '_blank');
        };
        // 初始化title
        const initialSettings = LS.get('tagManagerSettings', { searchEngine: 'google', customEngineUrl: '' });
        const engineNames = {
          google: 'Google',
          baidu: 'Baidu',
          bing: 'Bing',
          'yahoo-jp': 'Yahoo Japan',
          goo: 'Goo',
          sogou: 'Sogou',
          custom: '自定义'
        };
        googleBtn.title = `在 ${engineNames[initialSettings.searchEngine]} 中搜索此标签`;
        btnContainer.appendChild(googleBtn);

        // 排除按钮
        const deleteBtn = document.createElement('button');
        deleteBtn.style.cssText = 'padding:0 4px;border:none;border-radius:3px;background:#f56c6c;color:#fff;cursor:pointer;font-size:10px;flex-shrink:0;';
        deleteBtn.textContent = '✕';
        deleteBtn.title = '将此标签添加到排除列表';
        deleteBtn.onclick = (e) => {
          e.stopPropagation();
          // 将标签添加到排除列表
          excludeTagsSet.add(tag);
          excludeTagsWithTime[tag] = Date.now();
          // 保存到 localStorage
          const tagsStr = Array.from(excludeTagsSet).join(',');
          LS.set('excludeTags', tagsStr);
          LS.set('excludeTagsWithTime', excludeTagsWithTime);
          // 从待翻译列表中删除该标签
          const lines = transInput.value.split('\n');
          const filtered = lines.filter(line => line.trim() !== tag);
          transInput.value = filtered.join('\n');
          updateTransInputList();
          updateExcludeList();
          updateTransResultAfterExclude();
          showToast(`✅ 已排除: ${tag}`);
        };
        btnContainer.appendChild(deleteBtn);

        div.appendChild(btnContainer);

        transInputList.appendChild(div);
      });
    };

    // 监听textarea的变化
    transInput.addEventListener('input', updateTransInputList);

    // 监听搜索框的变化
    const searchInput = box.querySelector('#pteSavedSearch');
    searchInput.addEventListener('input', () => {
      const searchText = searchInput.value;
      const items = box.querySelectorAll('#pteSavedList > div');
      items.forEach(item => {
        const transSpan = item.querySelector('.pteTransDisplay');
        if (transSpan) {
          const trans = transSpan.textContent;
          // 只搜索翻译后的标签
          const matches = pinyinMatch(trans, searchText);
          item.style.display = matches ? 'flex' : 'none';
        }
      });
    });

    // 手动添加待翻译标签
    const manualTransInput = box.querySelector('#pteManualTransInput');
    const manualTransAddBtn = box.querySelector('#pteManualTransAdd');

    manualTransAddBtn.onclick = () => {
      const tag = manualTransInput.value.trim();
      if (!tag) {
        showToast('❌ 请输入标签');
        return;
      }

      // 检查是否已存在
      const existingTags = transInput.value.trim().split('\n').map(t => t.trim()).filter(Boolean);
      if (existingTags.includes(tag)) {
        showToast(`⚠️ 标签已存在`);
        return;
      }

      // 检查是否在排除列表中
      if (excludeTagsSet.has(tag)) {
        showToast(`❌ 此标签在排除列表中`);
        return;
      }

      // 添加标签
      if (transInput.value.trim()) {
        transInput.value += '\n' + tag;
      } else {
        transInput.value = tag;
      }

      manualTransInput.value = '';
      updateTransInputList();
      addOperationLog('手动添加待翻译', tag);
      updateOperationHistory();
      showToast(`✅ 已添加待翻译标签:${tag}`);
      manualTransInput.focus();
    };

    // 回车键添加
    manualTransInput.addEventListener('keypress', (e) => {
      if (e.key === 'Enter') {
        manualTransAddBtn.click();
      }
    });

    // 快速排除按钮 - 将待翻译栏中的标签添加到排除列表
    box.querySelector('#pteQuickExclude').onclick = () => {
      const tags = transInput.value.trim().split('\n').map(t => t.trim()).filter(Boolean);
      if (!tags.length) {
        showToast('待翻译栏无标签');
        return;
      }
      tags.forEach(t => {
        excludeTagsSet.add(t);
        excludeTagsWithTime[t] = Date.now();
      });
      const tagsStr = Array.from(excludeTagsSet).join(',');
      LS.set('excludeTags', tagsStr);
      LS.set('excludeTagsWithTime', excludeTagsWithTime);
      transInput.value = '';
      updateTransInputList();
      updateExcludeList();
      updateTransResultAfterExclude();
      addOperationLog('快速排除标签', `${tags.length} 个`);
      updateOperationHistory();
      showToast(`✅ 已将 ${tags.length} 个标签加入排除列表`);
    };

    // 提取选中作品标签
    box.querySelector('#pteExtractTags').onclick = async () => {
      const checkboxes = document.querySelectorAll('.pxe-mini-checkbox:checked');
      if (!checkboxes.length) {
        showToast('请先勾选要导入的作品');
        return;
      }

      const extractBtn = box.querySelector('#pteExtractTags');
      extractBtn.disabled = true;
      extractBtn.textContent = '⏳ 提取中...';

      const allTags = new Set();
      let processed = 0;
      let failed = 0;
      const totalWorks = checkboxes.length;

      // 从作业提取标签
      for (const checkbox of checkboxes) {
        try {
          // 获取作品ID的多种方式
          let illustId = null;

          // 方法1:从checkbox的value属性获取
          if (checkbox.value && /^\d+$/.test(checkbox.value)) {
            illustId = checkbox.value;
          }

          // 方法2:从checkbox的data-id属性获取
          if (!illustId && checkbox.dataset.id) {
            illustId = checkbox.dataset.id;
          }

          // 方法3:从nearby的 a 标签获取ID
          if (!illustId) {
            const link = checkbox.closest('[class*="item"], [class*="illust"], article')?.querySelector('a[href*="/artworks/"]');
            if (link) {
              const match = link.href.match(/\/artworks\/(\d+)/);
              if (match) illustId = match[1];
            }
          }

          // 方法4:从 name 属性获取(有些 checkbox 的 name 可能包含 ID)
          if (!illustId && checkbox.name) {
            const match = checkbox.name.match(/\d+/);
            if (match) illustId = match[0];
          }

          if (!illustId) {
            debugLog('EXTRACT', '无法获取作品ID', { checkboxValue: checkbox.value });
            console.warn('无法获取作品ID');
            failed++;
            extractBtn.textContent = `已提取${processed}/${totalWorks}`;
            continue;
          }

          // 官方API获取信息
          try {
            // 随机延迟300-800ms,防止请求过快导致被限流
            const delay = Math.random() * 500 + 300;
            await new Promise(resolve => setTimeout(resolve, delay));

            const info = await getJSON(`https://www.pixiv.net/ajax/illust/${illustId}`);
            if (info.body?.tags?.tags) {
              const tagList = info.body.tags.tags;
              // 只提取原始标签 (t.tag),不使用翻译
              const tags = tagList.map(t => t?.tag).filter(Boolean);
              debugLog('EXTRACT', `作品 ${illustId} 提取标签`, { tagsCount: tags.length, tags: tags });
              tags.forEach(t => allTags.add(t));
              processed++;
              extractBtn.textContent = `已提取${processed}/${totalWorks}`;
            } else {
              failed++;
              extractBtn.textContent = `已提取${processed}/${totalWorks}`;
            }
          } catch (e) {
            console.error(`获取作品 ${illustId} 的标签失败:`, e);
            failed++;
            extractBtn.textContent = `已提取${processed}/${totalWorks}`;
          }
        } catch (e) {
          console.error('提取标签失败:', e);
          failed++;
          extractBtn.textContent = `已提取${processed}/${totalWorks}`;
        }
      }

      if (allTags.size === 0) {
        showToast(`未能提取到标签(成功${processed}件,失败${failed}件)。请检查:\n1. 是否在 Pixiv 列表页\n2. 是否正确勾选了作品`);
        extractBtn.disabled = false;
        extractBtn.textContent = '提取标签';
        return;
      }

      // 检查是否已翻译
      const savedTranslations = LS.get('tagTranslations', {});
      const savedTagsList = Object.keys(savedTranslations);

      // 提取标签到输入框
      const existingTags = transInput.value.trim().split('\n').filter(Boolean);

      // 直接从 allTags 中移除已保存的标签(不区分大小写)
      const tagsToFilter = new Set();
      allTags.forEach(t => {
        const isSaved = savedTagsList.some(st => lower(st) === lower(t));
        const isExcluded = Array.from(excludeTagsSet).some(ex => {
          const lowerEx = lower(ex);
          return lower(t).includes(lowerEx) || lowerEx.includes(lower(t));
        });
        if (!isSaved && !isExcluded) {
          tagsToFilter.add(t);
        }
      });

      // 过滤:不重复(不在输入框中)
      const newTags = Array.from(tagsToFilter).filter(t => !existingTags.includes(t));

      // 最重要:以实际待翻译区为准
      const actualTransInputCount = transInput.value.trim().split('\n').filter(Boolean).length;

      // 重新梳理:所有标签应该被完整分类为:已保存 + 已排除 + 待翻译 + 新标签
      // 使用互斥分类方式,结合 allTags 和实际待翻译区
      const allTagsInUse = new Set([...Array.from(allTags), ...existingTags]);  // 合并 allTags 和实际待翻译区的标签

      const tagClassification = {};  // 记录每个标签的分类

      Array.from(allTagsInUse).forEach(t => {
        // 先检查是否已保存(不区分大小写)
        const isSaved = savedTagsList.some(st => lower(st) === lower(t));
        if (isSaved) {
          tagClassification[t] = 'saved';
        } else {
          // 再检查是否已排除(精确匹配)
          const lowerTag = lower(t);
          const isExcluded = Array.from(excludeTagsSet).some(ex => {
            return lower(ex) === lowerTag;
          });
          if (isExcluded) {
            tagClassification[t] = 'excluded';
          } else if (existingTags.includes(t)) {
            tagClassification[t] = 'existing';
          } else {
            tagClassification[t] = 'new';
          }
        }
      });

      // 计算各分类数量
      const classifiedSaved = Object.entries(tagClassification).filter(([_, c]) => c === 'saved');
      const classifiedExcluded = Object.entries(tagClassification).filter(([_, c]) => c === 'excluded');
      const classifiedExisting = Object.entries(tagClassification).filter(([_, c]) => c === 'existing');
      const classifiedNew = Object.entries(tagClassification).filter(([_, c]) => c === 'new');

      const savedCount = classifiedSaved.length;
      const excludedCount = classifiedExcluded.length;
      const existingInTransInputCount = classifiedExisting.length;
      const totalTagsCount = allTagsInUse.size;

      // Debug 日志:标签分类结果
      debugLog('EXTRACT', '标签分类完成', {
        total: totalTagsCount,
        new: classifiedNew.length,
        saved: classifiedSaved.length,
        excluded: classifiedExcluded.length,
        existing: classifiedExisting.length,
        savedTags: classifiedSaved.map(([t, _]) => t), // 显示具体的已保存标签
        excludedTags: classifiedExcluded.map(([t, _]) => t), // 显示具体的已排除标签
        newTags: classifiedNew.map(([t, _]) => t).slice(0, 10) // 只显示前10个
      });

      if (classifiedNew.length === 0) {
        const messages = [`所有标签已处理 (共${totalTagsCount}个)`];
        if (savedCount > 0) messages.push(`${savedCount}个已保存`);
        if (excludedCount > 0) messages.push(`${excludedCount}个已排除`);
        if (existingInTransInputCount > 0) messages.push(`${existingInTransInputCount}个待翻译区中已有`);

        // 清空待翻译区(因为所有标签都已处理)
        transInput.value = '';

        // 记录操作
        addOperationLog('提取标签', `无新标签(共${totalTagsCount}个:${savedCount}个已保存,${excludedCount}个已排除,${existingInTransInputCount}个待翻译)`);
        updateOperationHistory();
        // 确保待翻译列表显示
        updateTransInputList();
        showToast(messages.join(','));
      } else {
        // 只添加新标签,清空之前的内容
        const newTagsList = classifiedNew.map(([t, _]) => t);
        transInput.value = newTagsList.join('\n');
        updateTransInputList();
        const msgParts = [`✅ 已提取 ${classifiedNew.length} 个新标签(共${totalTagsCount}个)`];
        if (savedCount > 0) msgParts.push(`${savedCount}个已保存被过滤`);
        if (excludedCount > 0) msgParts.push(`${excludedCount}个已排除`);
        if (existingInTransInputCount > 0) msgParts.push(`${existingInTransInputCount}个待翻译区中已有`);
        msgParts.push(`成功${processed}个作品`);
        addOperationLog('提取标签', `${classifiedNew.length}个新标签,共${totalTagsCount}个(${savedCount}个已保存,${excludedCount}个已排除,${existingInTransInputCount}个待翻译)`);
        updateOperationHistory();
        showToast(msgParts.join(','));
      }

      extractBtn.disabled = false;
      extractBtn.textContent = '提取标签';
    };

    box.querySelector('#pteTranslateAll').onclick = async () => {
      const tags = transInput.value.split('\n').map(t => t.trim()).filter(Boolean);
      if (!tags.length) {
        showToast('请输入至少一个标签');
        return;
      }

      const btn = box.querySelector('#pteTranslateAll');
      btn.disabled = true;
      btn.textContent = '⏳ 翻译中...';
      transResult.innerHTML = '';

      for (const tag of tags) {
        // 直接调用 AI 翻译
        let translation = await translateWithQwen(tag, 'zh');

        transResult.innerHTML += `
          <div style="padding:6px 8px;border-bottom:1px solid #e0e0e0;display:flex;align-items:center;gap:6px;font-size:11px;">
            <span style="color:#1f6fff;font-weight:600;min-width:50px;max-width:50px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="${tag}">${tag}</span>
            <span style="color:#999;">→</span>
            <input type="text" value="${translation}" class="pteTransEdit" data-original="${tag}" style="max-width:90px;padding:2px 4px;border:1px solid #d9d9d9;border-radius:3px;font-size:11px;box-sizing:border-box;" />
            <button class="pteSaveOne" data-tag="${tag}" style="padding:2px 6px;border:none;border-radius:3px;background:#67c23a;color:#fff;cursor:pointer;font-size:10px;flex-shrink:0;">保存</button>
          </div>
        `;
      }

      btn.disabled = false;
      btn.textContent = '🚀 翻译全部';

      // 更新"保存全部"按钮的状态
      const saveAllBtn = box.querySelector('#pteSaveAll');
      const hasTranslations = transResult.querySelectorAll('.pteTransEdit').length > 0;
      saveAllBtn.disabled = !hasTranslations;
      saveAllBtn.style.opacity = hasTranslations ? '1' : '0.5';
      saveAllBtn.style.cursor = hasTranslations ? 'pointer' : 'not-allowed';

      // 绑定保存按钮
      transResult.querySelectorAll('.pteSaveOne').forEach(saveBtn => {
        saveBtn.onclick = () => {
          const tag = saveBtn.dataset.tag;
          const input = transResult.querySelector(`input[data-original="${tag}"]`);
          const translation = input.value.trim();
          if (!translation) {
            showToast('翻译不能为空');
            return;
          }
          savedTags[tag] = { translation: translation, timestamp: Date.now() };
          LS.set('tagTranslations', savedTags);
          // 从待翻译区移除该标签
          const lines = transInput.value.split('\n');
          const filtered = lines.filter(line => line.trim() !== tag);
          transInput.value = filtered.join('\n');
          // 更新所有相关UI
          updateSavedList();
          updateTransInputList();
          // 保存后重新应用搜索过滤
          const searchInput = box.querySelector('#pteSavedSearch');
          if (searchInput && searchInput.value.trim()) {
            searchInput.dispatchEvent(new Event('input'));
          }
          updateTransResultAfterExclude();
          addOperationLog('保存翻译', tag);
          updateOperationHistory();
          showToast(`✅ 已保存:${tag}`);

          // 更新"保存全部"按钮的状态
          const saveAllBtn = box.querySelector('#pteSaveAll');
          const hasTranslations = transResult.querySelectorAll('.pteTransEdit').length > 0;
          saveAllBtn.disabled = !hasTranslations;
          saveAllBtn.style.opacity = hasTranslations ? '1' : '0.5';
          saveAllBtn.style.cursor = hasTranslations ? 'pointer' : 'not-allowed';
        };
      });

      addOperationLog('翻译标签', `${tags.length} 个`);
      updateOperationHistory();
      showToast(`✅ 翻译完成(${tags.length} 个)`);
    };

    // 清空翻译结果
    box.querySelector('#pteClearTransResult').onclick = () => {
      // 打开标签管理设置功能
      const settingsDialog = document.createElement('div');
      settingsDialog.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border:2px solid #409eff;border-radius:8px;padding:20px;z-index:2147483648;box-shadow:0 4px 16px rgba(0,0,0,0.2);min-width:400px;max-width:600px;';
      settingsDialog.innerHTML = `
        <div style="font-weight:600;color:#1f6fff;margin-bottom:16px;font-size:14px;">⚙️ 标签管理设置</div>
        <div style="color:#666;margin-bottom:16px;font-size:12px;line-height:1.8;">
          <div style="margin-bottom:12px;">
            <label style="display:block;margin-bottom:6px;font-weight:600;">搜索引擎选择</label>
            <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px;">
              <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
                <input type="radio" name="searchEngine" value="google" style="cursor:pointer;" />
                <span>Google</span>
              </label>
              <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
                <input type="radio" name="searchEngine" value="baidu" style="cursor:pointer;" />
                <span>Baidu</span>
              </label>
              <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
                <input type="radio" name="searchEngine" value="bing" style="cursor:pointer;" />
                <span>Bing</span>
              </label>
              <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
                <input type="radio" name="searchEngine" value="yahoo-jp" style="cursor:pointer;" />
                <span>Yahoo Japan</span>
              </label>
              <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
                <input type="radio" name="searchEngine" value="goo" style="cursor:pointer;" />
                <span>Goo</span>
              </label>
              <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
                <input type="radio" name="searchEngine" value="sogou" style="cursor:pointer;" />
                <span>Sogou</span>
              </label>
              <label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
                <input type="radio" name="searchEngine" value="custom" style="cursor:pointer;" />
                <span>自定义</span>
              </label>
            </div>
            <div style="color:#999;font-size:11px;margin-bottom:8px;">用于待翻译区的搜索按钮</div>
            <div style="display:none;" id="customEngineDiv" style="margin-bottom:8px;">
              <label style="display:block;margin-bottom:4px;font-size:11px;font-weight:600;">自定义搜索 URL</label>
              <div style="display:flex;gap:6px;align-items:center;margin-bottom:6px;">
                <input type="text" id="customEngineUrl" placeholder="输入网站名称或完整 URL" style="flex:1;padding:6px;border:1px solid #d9d9d9;border-radius:3px;font-size:11px;box-sizing:border-box;" />
                <button id="customEnginePresets" style="padding:6px 12px;border:1px solid #d9d9d9;border-radius:3px;background:#f5f5f5;color:#666;cursor:pointer;font-size:11px;white-space:nowrap;">📋 内置</button>
              </div>
              <div id="presetMenu" style="display:none;position:absolute;background:#fff;border:1px solid #d9d9d9;border-radius:3px;box-shadow:0 2px 8px rgba(0,0,0,0.15);z-index:10000;min-width:200px;max-height:300px;overflow-y:auto;">
                <div style="padding:6px;">
                  <div style="padding:6px;cursor:pointer;hover:background:#f0f0f0;" data-preset="pixiv-dic">Pixiv百科</div>
                  <hr style="margin:4px 0;border:none;border-top:1px solid #e0e0e0;" />
                  <div style="padding:6px;cursor:pointer;hover:background:#f0f0f0;" data-preset="bing-translate">Bing翻译</div>
                  <div style="padding:6px;cursor:pointer;hover:background:#f0f0f0;" data-preset="baidu">百度翻译</div>
                  <div style="padding:6px;cursor:pointer;hover:background:#f0f0f0;" data-preset="deepl">DeepL翻译</div>
                  <div style="padding:6px;cursor:pointer;hover:background:#f0f0f0;" data-preset="google-translate">Google翻译</div>
                  <div style="padding:6px;cursor:pointer;hover:background:#f0f0f0;" data-preset="youdao">有道翻译</div>
                </div>
              </div>
              <div style="color:#999;font-size:10px;margin-top:8px;">💡 URL 格式说明:<br/>格式:https://site.com/search?q={tag}<br/>其中 {tag} 会被替换为搜索词</div>
              <div style="color:#999;font-size:10px;margin-top:4px;">💡 提示:输入网站名称(如 "pixiv")会自动识别,或输入完整 URL</div>
            </div>
          </div>
          <div style="margin-bottom:12px;">
            <label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
              <input type="checkbox" id="settingDebugMode" style="cursor:pointer;" />
              <span style="cursor:default;">Debug 模式</span>
            </label>
            <div style="color:#999;font-size:11px;margin-left:24px;">启用后会在浏览器控制台输出详细日志</div>
          </div>
        </div>
        <div style="display:flex;gap:10px;justify-content:flex-end;">
          <button id="settingsCancel" style="padding:8px 16px;border:1px solid #d9d9d9;border-radius:6px;background:#f5f5f5;color:#666;cursor:pointer;font-weight:600;font-size:12px;">取消</button>
          <button id="settingsSave" style="padding:8px 16px;border:none;border-radius:6px;background:#409eff;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">保存设置</button>
        </div>
      `;

      const settingsMask = document.createElement('div');
      settingsMask.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:2147483646;';

      document.body.appendChild(settingsMask);
      document.body.appendChild(settingsDialog);

      // 加载当前设置
      const currentSettings = LS.get('tagManagerSettings', { searchEngine: 'google', customEngineUrl: '', debugMode: false });

      const searchEngineRadios = settingsDialog.querySelectorAll('input[name="searchEngine"]');
      searchEngineRadios.forEach(radio => {
        if (radio.value === currentSettings.searchEngine) {
          radio.checked = true;
        }
      });

      const customEngineDiv = settingsDialog.querySelector('#customEngineDiv');
      const customEngineInput = settingsDialog.querySelector('#customEngineUrl');
      customEngineInput.value = currentSettings.customEngineUrl || '';

      // 切换自定义引擎输入框的显示
      searchEngineRadios.forEach(radio => {
        radio.addEventListener('change', () => {
          if (radio.value === 'custom') {
            customEngineDiv.style.display = 'block';
          } else {
            customEngineDiv.style.display = 'none';
          }
        });
      });

      // 初始化显示状态
      if (currentSettings.searchEngine === 'custom') {
        customEngineDiv.style.display = 'block';
      }

      // 预设菜单功能
      const presetMenu = settingsDialog.querySelector('#presetMenu');
      const presetsBtn = settingsDialog.querySelector('#customEnginePresets');
      const presetItems = presetMenu.querySelectorAll('[data-preset]');

          customEnginePresets.onclick = (e) => {
            e.stopPropagation();
            const presetMenu = settingsDialog.querySelector('#presetMenu');
            presetMenu.style.display = presetMenu.style.display === 'none' ? 'block' : 'none';
          };

          customEngineInput.addEventListener('click', (e) => {
            e.stopPropagation();
          });

      presetItems.forEach(item => {
        item.onclick = () => {
          const preset = item.dataset.preset;
          // 根据预设值直接填入对应的URL
          const presetUrls = {
            'pixiv-dic': 'https://dic.pixiv.net/a/{tag}',
            'wiki-ja': 'https://ja.wikipedia.org/wiki/{tag}',
            'moegirl': 'https://zh.moegirl.org.cn/{tag}',
            'bluearchive': 'https://wiki.biligame.com/ba/Students',
            'bluearchive-gk': 'https://www.gamekee.com/ba/',
            'azurlane': 'https://wiki.biligame.com/azurlane/{tag}',
            'bilibili-wiki': 'https://wiki.biligame.com/{tag}',
            'google-translate': 'https://translate.google.com/?text={tag}',
            'deepl': 'https://www.deepl.com/translator#en/ja/{tag}',
            'youdao': 'https://fanyi.youdao.com/#/text?text={tag}',
            'baidu': 'https://fanyi.baidu.com/#en/zh/{tag}',
            'bing-translate': 'https://www.bing.com/translator'
          };
          customEngineInput.value = presetUrls[preset] || preset;
          presetMenu.style.display = 'none';
        };
      });

      // 点击其他地方关闭菜单
      document.addEventListener('click', (e) => {
        if (!customEngineDiv.contains(e.target)) {
          presetMenu.style.display = 'none';
        }
      });

      const debugCheckbox = settingsDialog.querySelector('#settingDebugMode');
      debugCheckbox.checked = currentSettings.debugMode || false;

      // 保存按钮
      settingsDialog.querySelector('#settingsSave').onclick = () => {
        const selectedEngine = settingsDialog.querySelector('input[name="searchEngine"]:checked').value;
        let customUrl = customEngineInput.value.trim();

        // 如果选择自定义且输入了URL,自动识别和补全
        if (selectedEngine === 'custom' && customUrl) {
          // 自动识别常见的搜索引擎(支持模糊匹配)
          const urlPatterns = [
            { keywords: ['pixiv', 'dic', '百科'], url: 'https://dic.pixiv.net/a/{tag}' },
            { keywords: ['wiki', '维基'], url: 'https://ja.wikipedia.org/wiki/{tag}' },
            { keywords: ['bilibili', '网页'], url: 'https://wiki.biligame.com/{tag}' },
            { keywords: ['google', '谷歌'], url: 'https://translate.google.com/?text={tag}' },
            { keywords: ['deepl', '深蓝'], url: 'https://www.deepl.com/translator#en/ja/{tag}' },
            { keywords: ['youdao', '有道'], url: 'https://fanyi.youdao.com/#/text?text={tag}' },
            { keywords: ['baidu', '百度'], url: 'https://fanyi.baidu.com/#en/zh/{tag}' },
            { keywords: ['bing', '必应'], url: 'https://www.bing.com/translator' }
          ];

          const lowerInput = customUrl.toLowerCase();

          // 检查输入是否匹配任何已知的模式(模糊匹配)
          for (const pattern of urlPatterns) {
            if (pattern.keywords.some(keyword => lowerInput.includes(keyword))) {
              customUrl = pattern.url;
              break;
            }
          }
        }

        const settings = {
          searchEngine: selectedEngine,
          customEngineUrl: customUrl,
          debugMode: debugCheckbox.checked
        };
        LS.set('tagManagerSettings', settings);
        debugLog('STORAGE', 'localStorage 保存设置', { tagManagerSettings: settings });
        settingsDialog.remove();
        settingsMask.remove();
        showToast('✅ 设置已保存');
        addOperationLog('修改设置', '标签管理设置');
        updateOperationHistory();
      };

      // 取消按钮
      settingsDialog.querySelector('#settingsCancel').onclick = () => {
        settingsDialog.remove();
        settingsMask.remove();
      };

      settingsMask.onclick = () => {
        settingsDialog.remove();
        settingsMask.remove();
      };
    };

    // 导出列表(待翻译区的标签,JSON格式,无翻译结果)
    box.querySelector('#pteListExport').onclick = () => {
      const lines = transInput.value.split('\n').filter(line => line.trim());
      if (lines.length === 0) {
        showToast('待翻译区没有标签');
        return;
      }
      // 构建JSON对象,格式与导出翻译结果一致
      const tagsData = {};
      lines.forEach(tag => {
        tagsData[tag.trim()] = '';  // 值为空,表示未翻译
      });
      const data = {
        savedTags: tagsData
      };
      const json = JSON.stringify(data, null, 2);
      const blob = new Blob([json], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `PTE待翻译标签.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      addOperationLog('导出待翻译标签', `${lines.length} 个`);
      updateOperationHistory();
      showToast(`✅ 已导出 ${lines.length} 个待翻译标签`);
    };

    // 导入列表(从JSON文件导入标签到待翻译区)
    box.querySelector('#pteListImport').onclick = () => {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.json';
      input.onchange = (e) => {
        const file = e.target.files[0];
        if (file) {
          const reader = new FileReader();
          reader.onload = (event) => {
            try {
              const data = JSON.parse(event.target.result);
              const tagsToImport = data.savedTags || data;

              if (!tagsToImport || Object.keys(tagsToImport).length === 0) {
                showToast('❌ 无效的JSON格式或没有标签数据');
                return;
              }

              // 获取当前待翻译区的标签
              const currentLines = transInput.value.split('\n').filter(line => line.trim());
              const currentSet = new Set(currentLines.map(l => l.trim()));

              // 添加新标签到待翻译区
              const newTags = Object.keys(tagsToImport).filter(tag => !currentSet.has(tag.trim()));

              if (newTags.length > 0) {
                const existingContent = transInput.value.trim();
                transInput.value = existingContent ? existingContent + '\n' + newTags.join('\n') : newTags.join('\n');
              }

              updateTransInputList();

              const totalImported = Object.keys(tagsToImport).length;
              const duplicates = totalImported - newTags.length;
              let message = `✅ 已导入 ${totalImported} 个待翻译标签`;
              if (duplicates > 0) {
                message += `(其中 ${duplicates} 个重复)`;
              }
              addOperationLog('导入待翻译标签', `${totalImported} 个`);
              updateOperationHistory();
              showToast(message);
            } catch (err) {
              showToast('❌ 文件解析失败或JSON格式错误');
            }
          };
          reader.readAsText(file);
        }
      };
      input.click();
    };

    // 保存所有翻译
    box.querySelector('#pteSaveAll').onclick = () => {
      const inputs = transResult.querySelectorAll('.pteTransEdit');
      if (!inputs.length) {
        showToast('没有翻译结果可保存');
        return;
      }
      let count = 0;
      const tagsToRemoveFromInput = [];
      inputs.forEach(input => {
        const tag = input.dataset.original;
        const translation = input.value.trim();
        // 检查是否在排除列表中
        if (!excludeTagsSet.has(tag) && translation) {
          savedTags[tag] = { translation: translation, timestamp: Date.now() };
          tagsToRemoveFromInput.push(tag);
          count++;
          // 保存后隐藏该翻译结果
          input.closest('div').style.display = 'none';
        }
      });
      // 从待翻译区移除已保存的标签
      if (tagsToRemoveFromInput.length > 0) {
        const lines = transInput.value.split('\n');
        const filtered = lines.filter(line => !tagsToRemoveFromInput.includes(line.trim()));
        transInput.value = filtered.join('\n');
      }
      LS.set('tagTranslations', savedTags);
      updateSavedList();
      updateTransInputList();
      updateTransResultAfterExclude();
      if (count > 0) {
        addOperationLog('保存翻译', `${count} 个`);
        updateOperationHistory();
        showToast(`✅ 已保存 ${count} 个翻译`);
      } else {
        showToast('❌ 没有可保存的翻译(排除列表中的标签不能保存)');
      }
    };


    // 左侧排除标签 - 导出
    box.querySelector('#pteExcludeExport').onclick = () => {
      // 导出排除标签列表
      const data = {
        excludeTags: Array.from(excludeTagsSet),
        excludeTagsWithTime: excludeTagsWithTime
      };
      const json = JSON.stringify(data, null, 2);
      const blob = new Blob([json], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `PTE排除标签.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      addOperationLog('导出排除标签', `${excludeTagsSet.size} 个`);
      updateOperationHistory();
      showToast(`✅ 已导出 ${excludeTagsSet.size} 个排除标签`);
    };

    // 左侧排除标签 - 导入
    const createFileInput = (callback) => {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.txt,.csv';
      input.onchange = (e) => {
        const file = e.target.files[0];
        if (file) {
          const reader = new FileReader();
          reader.onload = (event) => {
            callback(event.target.result);
          };
          reader.readAsText(file);
        }
      };
      input.click();
    };

    // 通用导入处理函数(必须在导入对话框初始化之前定义)
    const processImport = (content, type) => {
      if (type === 'exclude') {

        let tags = content.trim();

        if (tags.startsWith('[') && tags.endsWith(']')) {
          try {
            const parsed = JSON.parse(tags);
            if (Array.isArray(parsed)) {
              tags = parsed.join(',');
            }
          } catch (e) {}
        }

        tags = tags
          .replace(/^["']|["']$/gm, '')
          .replace(/[\n\r\t]+/g, ',')
          .replace(/\s*[,,]\s*/g, ',')
          .split(',')
          .map(t => t.trim())
          .filter(Boolean)
          .join(',');

        if (tags) {
          const tagCount = tags.split(',').length;
          const now = Date.now();
          tags.split(',').forEach(t => {
            const trimmedTag = t.trim();
            excludeTagsSet.add(trimmedTag);
            if (!excludeTagsWithTime[trimmedTag]) {
              excludeTagsWithTime[trimmedTag] = now;
            }
          });
          LS.set('excludeTagsWithTime', excludeTagsWithTime);
          updateExcludeList();
          updateTransResultAfterExclude();
          showToast(`✅ 已导入 ${tagCount} 个排除标签`);
        } else {
          showToast('❌ 内容为空或格式错误');
        }
      } else if (type === 'saved') {

        // JSON格式
        if (content.startsWith('{') && content.endsWith('}')) {
          try {
            const data = JSON.parse(content);
            if (data.savedTags && typeof data.savedTags === 'object') {
              const now = Date.now();
              let imported = 0;
              Object.keys(data.savedTags).forEach(key => {
                const value = data.savedTags[key];
                // 兼容旧格式和新格式
                if (typeof value === 'string') {
                  savedTags[key] = { translation: value, timestamp: now };
                } else if (typeof value === 'object' && value.translation) {
                  savedTags[key] = { translation: value.translation, timestamp: value.timestamp || now };
                }
                imported++;
              });
              if (imported > 0) {
                LS.set('tagTranslations', savedTags);
                updateSavedList();
                showToast(`✅ 已导入 ${imported} 个翻译`);
              } else {
                showToast('❌ JSON格式错误:没有有效的翻译数据');
              }
              return;
            }
          } catch (e) {
            // 失败继续文本
          }
        }

        // 文本模式
        const lines = content.trim().split('\n').filter(Boolean);
        let imported = 0;

        lines.forEach(line => {
          const [original, translation] = line.split('|').map(s => s.trim());
          if (original && translation) {
            imported++;
          }
        });

        if (imported === 0) {
          showToast('❌ 格式错误,应为:原始标签|翻译\n每行一条或JSON格式');
          return;
        }

        lines.forEach(line => {
          const [original, translation] = line.split('|').map(s => s.trim());
          if (original && translation) {
            savedTags[original] = { translation: translation, timestamp: Date.now() };
          }
        });

        LS.set('tagTranslations', savedTags);
        // 更新所有相关UI
        updateSavedList();
        updateTransResultAfterExclude();
        showToast(`✅ 已导入 ${imported} 个翻译`);
      }
    };


    const importDialog = box.querySelector('#pteImportDialog');
    const importMask = box.querySelector('#pteImportMask');
    const fileInput = box.querySelector('#pteFileImportInput');

    const showImportDialog = (type) => {
      currentImportType = type;
      importDialog.style.display = 'block';
      importMask.style.display = 'block';
    };

    const hideImportDialog = () => {
      importDialog.style.display = 'none';
      importMask.style.display = 'none';
      currentImportType = null;
    };

    // 左侧排除标签 - 导入按钮
    box.querySelector('#pteExcludeImport').onclick = () => {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.json';
      input.onchange = (e) => {
        const file = e.target.files[0];
        if (file) {
          const reader = new FileReader();
          reader.onload = (event) => {
            try {
              const data = JSON.parse(event.target.result);
              if (data.excludeTags && Array.isArray(data.excludeTags)) {
                // 清空待添加的数据
                excludeTagsSet.clear();
                // 添加新数据
                data.excludeTags.forEach(tag => {
                  excludeTagsSet.add(tag);
                });
                excludeTagsWithTime = data.excludeTagsWithTime || {};
                // 添加时间戳
                const now = Date.now();
                data.excludeTags.forEach(tag => {
                  if (!excludeTagsWithTime[tag]) {
                    excludeTagsWithTime[tag] = now;
                  }
                });
                // 保存数据
                const tagsStr = Array.from(excludeTagsSet).join(',');
                LS.set('excludeTags', tagsStr);
                LS.set('excludeTagsWithTime', excludeTagsWithTime);
                updateExcludeList();
                updateTransResultAfterExclude();
                addOperationLog('导入排除标签', `${data.excludeTags.length} 个`);
                updateOperationHistory();
                showToast(`✅ 已导入 ${data.excludeTags.length} 个排除标签`);
              } else {
                showToast('❌ JSON格式错误');
              }
            } catch (err) {
              showToast('❌ 文件解析失败');
            }
          };
          reader.readAsText(file);
        }
      };
      input.click();
    };

    // 右侧已保存翻译 - 导入按钮
    box.querySelector('#pteSavedImport').onclick = () => {
      const input = document.createElement('input');
      input.type = 'file';
      input.accept = '.json';
      input.onchange = (e) => {
        const file = e.target.files[0];
        if (file) {
          const reader = new FileReader();
          reader.onload = (event) => {
            try {
              const data = JSON.parse(event.target.result);
              if (data.savedTags && typeof data.savedTags === 'object') {
                // 确保有时间戳
                const now = Date.now();
                Object.keys(data.savedTags).forEach(key => {
                  const value = data.savedTags[key];
                  // 兼容旧格式和新格式
                  if (typeof value === 'string') {
                    data.savedTags[key] = { translation: value, timestamp: now };
                  } else if (typeof value === 'object' && !value.timestamp) {
                    value.timestamp = now;
                  }
                });
                // 合并数据
                Object.assign(savedTags, data.savedTags);
                LS.set('tagTranslations', savedTags);
                updateSavedList();
                addOperationLog('导入翻译', `${Object.keys(data.savedTags).length} 个`);
                updateOperationHistory();
                showToast(`✅ 已导入 ${Object.keys(data.savedTags).length} 个翻译`);
              } else {
                showToast('❌ JSON格式错误');
              }
            } catch (err) {
              showToast('❌ 文件解析失败');
            }
          };
          reader.readAsText(file);
        }
      };
      input.click();
    };

    // 导入对话框
    const confirmBtn = box.querySelector('#pteImportConfirm');
    if (confirmBtn) {
      confirmBtn.onclick = () => {
        const textarea = box.querySelector('#pteImportTextarea');
        const content = textarea.value.trim();
        if (!content) {
          showToast('❌ 请粘贴导入内容');
          return;
        }
        hideImportDialog();
        processImport(content, currentImportType);
      };
    } else {
      console.warn('导入确认按钮未找到');
    }

    const cancelBtn = box.querySelector('#pteImportCancel');
    if (cancelBtn) {
      cancelBtn.onclick = () => {
        hideImportDialog();
      };
    }

    // 遮罩点击关闭对话框
    importMask.onclick = hideImportDialog;

    // 处理文件输入
    fileInput.onchange = (e) => {
      const file = e.target.files[0];
      if (!file) return;

      const reader = new FileReader();
      reader.onload = (event) => {
        processImport(event.target.result, currentImportType);
      };
      reader.readAsText(file);
    };

    // 手动添加排除标签
    const manualInput = box.querySelector('#pteManualExcludeInput');
    const addBtn = box.querySelector('#pteManualExcludeAdd');

    addBtn.onclick = () => {
      const tag = manualInput.value.trim();
      if (!tag) {
        showToast('❌ 请输入标签');
        return;
      }
      excludeTagsSet.add(tag);
      excludeTagsWithTime[tag] = Date.now();
      // 立即保存到 localStorage
      const tagsStr = Array.from(excludeTagsSet).join(',');
      console.log('[PTE-DEBUG] 添加标签前:', localStorage.getItem('pxeMini:excludeTags'));
      LS.set('excludeTags', tagsStr);
      LS.set('excludeTagsWithTime', excludeTagsWithTime);
      console.log('[PTE-DEBUG] 添加标签后 tagsStr:', tagsStr);
      console.log('[PTE-DEBUG] 添加标签后 LS.get("excludeTags"):', LS.get('excludeTags'));
      console.log('[PTE-DEBUG] 添加标签后 localStorage.getItem:', localStorage.getItem('pxeMini:excludeTags'));
      updateExcludeList();
      updateTransResultAfterExclude();
      addOperationLog('添加排除标签', tag);
      updateOperationHistory();
      manualInput.value = '';
      showToast(`✅ 已添加:${tag}`);
    };

    manualInput.onkeypress = (e) => {
      if (e.key === 'Enter') {
        addBtn.click();
      }
    };

    // 排除标签排序
    const sortBtn = box.querySelector('#pteExcludeSort');
    const sortMenu = box.querySelector('#pteSortMenu');

    const sortModeLabels = {
      'alpha-asc': 'A→Z',
      'alpha-desc': 'Z→A',
      'time-new': '新→旧',
      'time-old': '旧→新'
    };

    const sortModeDesc = {
      'alpha-asc': 'A→Z',
      'alpha-desc': 'Z→A',
      'tag-asc': '标A→Z',
      'tag-desc': '标Z→A',
      'trans-asc': '译A→Z',
      'trans-desc': '译Z→A',
      'time-new': '新→旧',
      'time-old': '旧→新'
    };

    sortBtn.onclick = (e) => {
      e.stopPropagation();
      sortMenu.style.display = sortMenu.style.display === 'none' ? 'block' : 'none';
    };

    sortMenu.querySelectorAll('div[data-sort]').forEach(item => {
      item.onclick = (e) => {
        e.stopPropagation();
        const newMode = item.getAttribute('data-sort');
        excludeSortMode = newMode;
        LS.set('excludeSortMode', excludeSortMode);
        updateExcludeList();
        sortMenu.style.display = 'none';
        showToast(`✅ 已切换排序为: ${sortModeDesc[excludeSortMode]}`);
      };
    });

    // 点击页面其他地方关闭菜单
    document.addEventListener('click', (e) => {
      if (!sortBtn.contains(e.target) && !sortMenu.contains(e.target)) {
        sortMenu.style.display = 'none';
      }
    });

    // 已保存翻译排序
    const savedSortBtn = box.querySelector('#pteSavedSort');
    const savedSortMenu = box.querySelector('#pteSavedSortMenu');
    let savedSortMode = LS.get('savedSortMode', 'alpha-asc') || 'alpha-asc';

    savedSortBtn.onclick = (e) => {
      e.stopPropagation();
      savedSortMenu.style.display = savedSortMenu.style.display === 'none' ? 'block' : 'none';
    };

    savedSortMenu.querySelectorAll('div[data-sort]').forEach(item => {
      item.onclick = (e) => {
        e.stopPropagation();
        const newMode = item.getAttribute('data-sort');
        savedSortMode = newMode;
        LS.set('savedSortMode', savedSortMode);
        updateSavedList();
        savedSortMenu.style.display = 'none';
        showToast(`✅ 已切换排序为: ${sortModeDesc[savedSortMode]}`);
      };
    });

    // 点击页面其他地方关闭已保存翻译的排序菜单
    document.addEventListener('click', (e) => {
      if (!savedSortBtn.contains(e.target) && !savedSortMenu.contains(e.target)) {
        savedSortMenu.style.display = 'none';
      }
    });

    // 左侧排除标签 - 保存
    box.querySelector('#pteExcludeSave').onclick = () => {
      const tags = Array.from(excludeTagsSet).join(',');
      CFG.filters.excludeTags = tags;
      try { LS.set('excludeTags', tags); } catch { }
      LS.set('excludeTagsWithTime', excludeTagsWithTime);
      LS.set('excludeSortMode', excludeSortMode);
      const sortModeNames = { 'alpha-asc': 'A→Z', 'alpha-desc': 'Z→A', 'time-new': '新→旧', 'time-old': '旧→新' };
      addOperationLog('保存排除标签', `${excludeTagsSet.size} 个`);
      updateOperationHistory();
      showToast(`✅ 已保存 ${excludeTagsSet.size} 个排除标签(排序: ${sortModeNames[excludeSortMode]})`);
    };

    // 清空前要求确认
    box.querySelector('#pteExcludeReset').onclick = () => {
      // 创建自定义确认对话框
      const confirmDialog = document.createElement('div');
      confirmDialog.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border:2px solid #f56c6c;border-radius:8px;padding:20px;z-index:2147483648;box-shadow:0 4px 16px rgba(0,0,0,0.2);min-width:300px;';
      confirmDialog.innerHTML = `
        <div style="font-weight:600;color:#f56c6c;margin-bottom:16px;font-size:14px;">⚠️ 确认清空</div>
        <div style="color:#666;margin-bottom:20px;font-size:12px;">确定要清空所有排除标签吗?此操作无法撤销。</div>
        <div style="display:flex;gap:10px;justify-content:flex-end;">
          <button id="pteConfirmCancel" style="padding:8px 16px;border:1px solid #d9d9d9;border-radius:6px;background:#f5f5f5;color:#666;cursor:pointer;font-weight:600;font-size:12px;">取消</button>
          <button id="pteConfirmOk" style="padding:8px 16px;border:none;border-radius:6px;background:#f56c6c;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">清空</button>
        </div>
      `;

      const mask = document.createElement('div');
      mask.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:2147483647;';

      document.body.appendChild(mask);
      document.body.appendChild(confirmDialog);

      const cancelBtn = confirmDialog.querySelector('#pteConfirmCancel');
      const okBtn = confirmDialog.querySelector('#pteConfirmOk');

      const cleanup = () => {
        document.body.removeChild(mask);
        document.body.removeChild(confirmDialog);
      };

      cancelBtn.onclick = cleanup;
      mask.onclick = cleanup;

      okBtn.onclick = () => {
        cleanup();
        // 执行清空操作
        excludeTagsSet.clear();
        excludeTagsWithTime = {};
        LS.set('excludeTagsWithTime', excludeTagsWithTime);
        CFG.filters.excludeTags = '';
        LS.set('excludeTags', '');
        updateExcludeList();
        updateTransResultAfterExclude();
        addOperationLog('清空排除标签', '');
        updateOperationHistory();
        showToast('✅ 已清空排除列表');
      };
    };

    // 导出已保存翻译
    box.querySelector('#pteSavedExport').onclick = () => {
      // 按当前排序模式导出
      const sortMode = LS.get('savedSortMode', 'tag-asc') || 'tag-asc';
      let entries = Object.entries(savedTags);

      if (sortMode === 'tag-asc') {
        entries.sort((a, b) => a[0].localeCompare(b[0]));
      } else if (sortMode === 'tag-desc') {
        entries.sort((a, b) => b[0].localeCompare(a[0]));
      } else if (sortMode === 'trans-asc') {
        entries.sort((a, b) => {
          const transA = typeof a[1] === 'string' ? a[1] : a[1].translation;
          const transB = typeof b[1] === 'string' ? b[1] : b[1].translation;
          return transA.localeCompare(transB);
        });
      } else if (sortMode === 'trans-desc') {
        entries.sort((a, b) => {
          const transA = typeof a[1] === 'string' ? a[1] : a[1].translation;
          const transB = typeof b[1] === 'string' ? b[1] : b[1].translation;
          return transB.localeCompare(transA);
        });
      } else if (sortMode === 'time-new') {
        entries.sort((a, b) => (b[1].timestamp || 0) - (a[1].timestamp || 0));
      } else if (sortMode === 'time-old') {
        entries.sort((a, b) => (a[1].timestamp || 0) - (b[1].timestamp || 0));
      }

      // 转为简化格式:只导出翻译文本,不包含时间戳
      const exportTags = {};
      entries.forEach(([key, value]) => {
        exportTags[key] = typeof value === 'string' ? value : value.translation;
      });

      const data = {
        savedTags: exportTags
      };
      const json = JSON.stringify(data, null, 2);
      const blob = new Blob([json], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = `PTE翻译结果.json`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      addOperationLog('导出翻译', `${Object.keys(savedTags).length} 个`);
      updateOperationHistory();
      showToast(`✅ 已导出 ${Object.keys(savedTags).length} 个翻译(按${sortMode}排序)`);
    };

    // 右侧已保存翻译 - 保存
    box.querySelector('#pteSavedSave').onclick = () => {
      const content = JSON.stringify(savedTags);
      try { LS.set('tagTranslations', content); } catch { }
      addOperationLog('保存翻译', `${Object.keys(savedTags).length} 个`);
      updateOperationHistory();
      showToast(`✅ 已保存 ${Object.keys(savedTags).length} 个翻译`);
    };

    // 清空已保存翻译
    box.querySelector('#pteSavedReset').onclick = () => {
      // 创建自定义确认对话框
      const confirmDialog = document.createElement('div');
      confirmDialog.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;border:2px solid #f56c6c;border-radius:8px;padding:20px;z-index:2147483648;box-shadow:0 4px 16px rgba(0,0,0,0.2);min-width:300px;';
      confirmDialog.innerHTML = `
        <div style="font-weight:600;color:#f56c6c;margin-bottom:16px;font-size:14px;">⚠️ 确认清空</div>
        <div style="color:#666;margin-bottom:20px;font-size:12px;">确定要清空所有已保存的翻译吗?此操作无法撤销。</div>
        <div style="display:flex;gap:10px;justify-content:flex-end;">
          <button id="pteConfirmCancel" style="padding:8px 16px;border:1px solid #d9d9d9;border-radius:6px;background:#f5f5f5;color:#666;cursor:pointer;font-weight:600;font-size:12px;">取消</button>
          <button id="pteConfirmOk" style="padding:8px 16px;border:none;border-radius:6px;background:#f56c6c;color:#fff;cursor:pointer;font-weight:600;font-size:12px;">清空</button>
        </div>
      `;

      const mask = document.createElement('div');
      mask.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);z-index:2147483647;';

      document.body.appendChild(mask);
      document.body.appendChild(confirmDialog);

      const cancelBtn = confirmDialog.querySelector('#pteConfirmCancel');
      const okBtn = confirmDialog.querySelector('#pteConfirmOk');

      const cleanup = () => {
        document.body.removeChild(mask);
        document.body.removeChild(confirmDialog);
      };

      cancelBtn.onclick = cleanup;
      mask.onclick = cleanup;

      okBtn.onclick = () => {
        cleanup();
        // 执行清空操作
        for (const key in savedTags) {
          delete savedTags[key];
        }
        LS.set('tagTranslations', savedTags);
        // 更新所有相关UI
        updateSavedList();
        updateTransResultAfterExclude();
        addOperationLog('清空翻译', '');
        updateOperationHistory();
        showToast('✅ 已清空已保存翻译');
      };
    };

    // 初始化操作历史显示
    updateOperationHistory();

    // 关闭前清空待翻译区的数据
    const saveBeforeClose = (e) => {
      e?.preventDefault?.();
      e?.stopPropagation?.();
      // 关闭时不保存,清空待翻译数据
      try {
        localStorage.removeItem(LSKEY + ':manualTags');
      } catch { }
      if (mask && mask.parentNode) {
        mask.parentNode.removeChild(mask);
      }
      return false;
    };

    // 关闭
    const closeBtn = box.querySelector('#pteManagerClose');
    if (closeBtn) {
      closeBtn.addEventListener('click', saveBeforeClose);
    }
    // 防止误触背景:改为需要点击关闭按钮才能关闭
    mask.addEventListener('click', (e) => { if (e.target === mask) e.stopPropagation(); });
  }


  /******************** 模式 & 本地下载工具 ********************/
  const COLOR = { eagle: '#409eff', disk: '#f1a72e' }; // 蓝(鹰) / 偏黄(本地)
  function fmtIndex(i, total) { const w = String(total).length; return String(i).padStart(w, '0'); }
  function inferExtFromUrl(u) {
    const m = u.match(/\.([a-zA-Z0-9]+)(?:\?|$)/); return m ? ('.' + m[1].toLowerCase()) : '.jpg';
  }

  function gmDownloadWithHeaders(url, name, headers) {
    // Disk 模式 + FS
    if (typeof PTE_FS !== 'undefined' && PTE_FS && PTE_FS.root && (typeof CFG === 'object') && CFG.mode === 'disk') {
      return (async () => {
        const ab = await gmFetchBinary(url, { headers: headers || {} });
        const blob = new Blob([ab]);
        await saveBlobAsWithPath(name, blob);
      })();
    }
    // 回退:GM_download(无法创建子目录,仅作兜底)
    return new Promise((resolve, reject) => {
      try {
        GM_download({
          url,
          name,
          saveAs: false,
          headers: headers || {},
          onload: resolve,
          onerror: reject,
          ontimeout: reject
        });
      } catch (e) { reject(e); }
    });
  }

  // ====== FS Access helpers (user-gesture required once) ======
  let PTE_FS = { root: null, picked: false };
  async function ptePickDownloadsRoot() {
    if (!('showDirectoryPicker' in window)) { showToast('当前浏览器不支持选择目录(需要 Chrome/Edge 版本较新)'); return false; }
    try {
      const root = await window.showDirectoryPicker({ id: 'pte-download-root', mode: 'readwrite', startIn: 'downloads' });
      PTE_FS.root = root; PTE_FS.picked = true;
      showToast('已选择下载目录:Downloads/Pixiv');
      return true;
    } catch (e) {
      console.warn('目录选择取消或失败', e);
      showToast('未选择目录,继续使用浏览器默认下载(无法创建子文件夹)');
      return false;
    }
  }
  async function pteSaveWithFS(path, blob) {
    if (!PTE_FS.root) return false;
    try {
      const parts = path.split('/').filter(Boolean);
      let dir = PTE_FS.root;
      for (let i = 0; i < parts.length - 1; i++) {
        dir = await dir.getDirectoryHandle(parts[i], { create: true });
      }
      const fname = parts[parts.length - 1];
      const fileHandle = await dir.getFileHandle(fname, { create: true });
      const writable = await fileHandle.createWritable();
      await writable.write(blob);
      await writable.close();
      return true;
    } catch (e) {
      console.warn('FS Access 写入失败,回退 GM_download', e);
      return false;
    }
  }

  async function saveBlobAsWithPath(path, blob) {
    const url = URL.createObjectURL(blob);
    try {
      if (PTE_FS.root) {
        const ok = await pteSaveWithFS(path, blob);
        if (ok) { URL.revokeObjectURL(url); return; }
      }
    } catch (e) { console.warn(e); }
    return new Promise((resolve, reject) => {
      const cleanup = () => { setTimeout(() => URL.revokeObjectURL(url), 2000); };
      try {
        GM_download({
          url, name: path, saveAs: false,
          onload: () => { cleanup(); resolve(); },
          onerror: (e) => { cleanup(); reject(e); },
          ontimeout: (e) => { cleanup(); reject(e); }
        });
      } catch (e) { cleanup(); reject(e); }
    });
  }

  // 统一请求处理
  function gmFetch(url, options = {}) {
    const { method = 'GET', body = null, headers = {}, responseType = 'text' } = options;
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method, url, data: body, headers,
        responseType: responseType === 'json' ? 'text' : responseType,
        onload: (res) => {
          if (responseType === 'json') {
            try { resolve(JSON.parse(res.responseText || '{}')); } catch { resolve({}); }
          } else {
            resolve(res.response || res.responseText);
          }
        },
        onerror: reject,
        ontimeout: reject
      });
    });
  }

  // 向后兼容的快捷函数
  function gmFetchBinary(url, options = {}) {
    return gmFetch(url, { ...options, responseType: 'arraybuffer' });
  }
  function gmFetchText(url, options = {}) {
    return gmFetch(url, { ...options, responseType: 'text' });
  }
  async function ensureFflateLoaded() {
    if (window.fflate) return;
    throw new Error('fflate 未加载(@require 失败)');
  }
  let __gifWorkerURL = null;
  async function ensureGifLibLoaded() {
    if (!window.GIF) throw new Error('gif.js 未加载(@require 失败)');
    if (!__gifWorkerURL) {
      const workerCode = await gmFetchText('https://cdn.jsdelivr.net/npm/[email protected]/dist/gif.worker.js');
      __gifWorkerURL = URL.createObjectURL(new Blob([workerCode], { type: 'text/javascript' }));
    }
  }
  function guessMime(name) { return name.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg'; }
  function decodeImageFromU8(u8, mime) {
    return new Promise((resolve, reject) => {
      const blob = new Blob([u8], { type: mime });
      const url = URL.createObjectURL(blob);
      const img = new Image();
      img.onload = () => { URL.revokeObjectURL(url); resolve(img); };
      img.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
      img.src = url;
    });
  }

  // GIF 转换逻辑
  const GifHelper = {
    async convertToGifBlob(artId) {
      await ensureFflateLoaded();
      await ensureGifLibLoaded();
      const meta = await ugoiraMeta(artId);
      const zipUrl = meta?.body?.originalSrc || meta?.body?.src;
      const frames = meta?.body?.frames || [];
      if (!zipUrl || !frames.length) throw new Error('无法获取动图元数据');
      const zipBuf = await gmFetchBinary(zipUrl, { responseType: 'arraybuffer', headers: { referer: 'https://www.pixiv.net/' } });
      const entries = window.fflate.unzipSync(new Uint8Array(zipBuf));
      const first = frames[0];
      const firstBytes = entries[first.file];
      if (!firstBytes) throw new Error('压缩包缺少首帧: ' + first.file);
      const firstImg = await decodeImageFromU8(firstBytes, guessMime(first.file));
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d', { willReadFrequently: true });
      canvas.width = firstImg.width; canvas.height = firstImg.height;
      const gif = new window.GIF({ workers: 2, quality: 10, width: canvas.width, height: canvas.height, workerScript: __gifWorkerURL });
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.drawImage(firstImg, 0, 0);
      gif.addFrame(ctx, { copy: true, delay: Math.max(20, first.delay || 100) });
      for (let i = 1; i < frames.length; i++) {
        const f = frames[i];
        const bytes = entries[f.file];
        if (!bytes) throw new Error('压缩包缺少帧: ' + f.file);
        const img = await decodeImageFromU8(bytes, guessMime(f.file));
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(img, 0, 0);
        gif.addFrame(ctx, { copy: true, delay: Math.max(20, f.delay || 100) });
      }
      const blob = await new Promise(resolve => { gif.on('finished', b => resolve(b)); gif.render(); });
      return blob;
    },

    async saveAndGetDataURL(artId, title, { saveLocal = true, savePath = null, needDataURL = true } = {}) {
      const blob = await this.convertToGifBlob(artId);
      const safeTitle = sanitize(title || '');
      const baseName = safeTitle || `pixiv_${artId}`;
      const trimmedBase = baseName.length > 80 ? baseName.slice(0, 80) : baseName;
      const name = `${trimmedBase}_${artId}.gif`;

      if (saveLocal) {
        if (savePath) {
          await saveBlobAsWithPath(savePath, blob);
        } else {
          saveBlobAs(name, blob);
        }
      }

      let dataURL = null;
      if (needDataURL) {
        dataURL = await blobToDataURL(blob);
      }

      return { blob, dataURL, name };
    }
  };
  function saveBlobAs(filename, blob) {
    const url = URL.createObjectURL(blob);
    const cleanup = () => setTimeout(() => URL.revokeObjectURL(url), 2000);
    try {
      if (typeof GM_download === 'function') {
        GM_download({ url, name: filename, saveAs: false, onload: cleanup, ontimeout: cleanup, onerror: () => { cleanup(); fallback(); } });
        return;
      }
    } catch { cleanup(); }
    fallback();
    function fallback() {
      const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); cleanup();
    }
  }

  function blobToDataURL(blob) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result);
      reader.onerror = reject;
      reader.readAsDataURL(blob);
    });
  }

  /******************** 导入 / 合并行为 ********************/
  async function importMode(mode) {
    cancel = false; aborters.clear();

    if (mode === 'one') {
      const id = location.pathname.match(/artworks\/(\d+)/)?.[1];
      if (!id) { showToast('未识别到作品ID'); return; }
      return importOne(id, /*mergeGif*/ true);
    }

    showScan();

    let ids = []; const onUser = isUser();

    if (mode === 'selected') {
      const cbs = [...document.querySelectorAll('.pxe-mini-checkbox:checked')];
      ids = [...new Set(cbs.map(cb => cb.dataset.id).filter(Boolean))];
      updScan(ids.length, 0, true);
    } else if (mode === 'page') {
      ids = collectIdsFromPage(); updScan(ids.length, 0, true);
    } else if (mode === 'all') {
      if (onUser) {
        const m = location.pathname.match(/users\/(\d+)/); if (!m) { closeScan(); showToast('未识别到用户ID'); return; }
        const uid = m[1]; ids = await allIllustIds(uid); updScan(ids.length, 0, true);
      } else {
        ids = collectIdsFromPage(); updScan(ids.length, 0, true);
      }
    }

    if (cancel) { closeScan(); return; }
    if (!ids.length) { closeScan(); showToast(mode === 'selected' ? '请先勾选作品' : '未在本页找到作品'); return; }

    // 修正标签中的引号和特殊字符
    const cleanExcludeTag = (tag) => {
      return lower(tag.trim().replace(/^["']|["']$/g, ''));
    };
    const wants = (CFG.filters.excludeTags || '')
      .split(',')
      .map(cleanExcludeTag)
      .filter(Boolean);
    const savedTranslations = LS.get('tagTranslations', {});

    // 标签优化流程
    const cleanTags = (tags) => {
      if (!wants.length) return tags || [];
      return (tags || []).filter(t => {
        const lowerTag = lower(t);
        return !wants.some(ex => lowerTag === ex);
      });
    };

    const processTagsWithTranslation = (tags) => {
      // 第1步:先过排除规则
      const filtered = cleanTags(tags || []);
      // 第2步:应用翻译
      const translated = filtered.map(t => {
        const trans = savedTranslations[t];
        if (!trans) return t;
        // 兼容旧格式(字符串)和新格式(对象)
        return typeof trans === 'string' ? trans : trans.translation || t;
      });
      // 第3步:去重
      return Array.from(new Set(translated));
    };

    closeScan(); showImport(ids.length); let kept = []; let done = 0; let ok = 0; updImport(0, ids.length, 0);
    for (const id of ids) {
      if (cancel) break;
      try {
        const info = await illustInfoAndPages(id); if (cancel) break;

        const baseCommon = { website: `https://www.pixiv.net/artworks/${id}` };
        const modTime = (CFG.feature.useUploadAsAddDate && info.uploadDate) ? new Date(info.uploadDate).getTime() : undefined;
        if (CFG.mode === 'eagle') {
          let items = [];
          if (info.illustType === 2) {
            // ugoira→GIF:优先 Eagle,超閾转本地
            const blob = await GifHelper.convertToGifBlob(id);
            if (blob.size > BIG_GIF_LIMIT) {
              const safeUser = sanitize(info.userName || ("Pixiv_" + info.userId));
              const baseDir = `Pixiv/${safeUser}_${info.userId}`;
              const savePath = `${baseDir}/${id}.gif`;
              await saveBlobAsWithPath(savePath, blob);
              bigGifFallbacks.push({ id, size: blob.size, path: savePath, userName: info.userName, userId: info.userId });
              ok++;
            } else {
              const safeTitle = sanitize(info.title || '');
              const baseName = safeTitle || `pixiv_${id}`;
              const trimmedBase = baseName.length > 80 ? baseName.slice(0, 80) : baseName;
              const name = `${trimmedBase}_${id}.gif`;
              const dataURL = await blobToDataURL(blob);
              const processedTags = processTagsWithTranslation(info.tags || []);
              const one = { url: dataURL, name: name, tags: Array.from(new Set([...processedTags, info.userName].filter(Boolean))) };
              if (modTime) one.modificationTime = modTime;
              items.push({ ...baseCommon, ...one });
            }
          } else {
            const rng = parseRange(CFG.filters.pageRange); const urls = info.pageUrls || [];
            let use = urls; if (rng) use = urls.filter((_, i) => { const p = i + 1; return p >= rng[0] && p <= rng[1]; });
            let i = 0;
            items = use.map(u => {
              const processedTags = processTagsWithTranslation(info.tags || []);
              const one = { url: u, name: use.length > 1 ? `${info.title}_p${++i}` : info.title, tags: Array.from(new Set([...processedTags, info.userName].filter(Boolean))), headers: { referer: 'https://www.pixiv.net/' } };
              if (modTime) one.modificationTime = modTime;
              return { ...baseCommon, ...one };
            });
          }
          const fid = await ensureArtistFolder(info.userId, info.userName);
          if (items.length) { await addToEagle(items, fid); ok++; }
        } else {
          // Disk 模式:保存到 Downloads/Pixiv/作者ID/ 目录
          const safeUser = sanitize(info.userName || ("Pixiv_" + info.userId));
          const baseDir = `Pixiv/${safeUser}_${info.userId}`;
          if (info.illustType === 2) {
            const savePath = `${baseDir}/${id}.gif`;
            await GifHelper.saveAndGetDataURL(id, info.title, { saveLocal: true, savePath, needDataURL: false });
            ok++;
          } else {
            const rng = parseRange(CFG.filters.pageRange); const urls = info.pageUrls || [];
            let use = urls; if (rng) use = urls.filter((_, i) => { const p = i + 1; return p >= rng[0] && p <= rng[1]; });
            const total = use.length || 1;
            for (let i = 0; i < use.length; i++) {
              const u = use[i]; const ext = inferExtFromUrl(u);
              const fname = total > 1 ? `${baseDir}/${id}_${fmtIndex(i + 1, total)}${ext}` : `${baseDir}/${id}${ext}`;
              await gmDownloadWithHeaders(u, fname, { referer: 'https://www.pixiv.net/' });
            }
            ok++;
          }
        }
      } catch (e) { console.warn('[导入失败]', id, e); }
      done++; updImport(done, ids.length, ok); await sleep(120); if (cancel) break;
    }
    const filtered = done - ok;
    let msg = cancel ? `已取消。处理${done},成功${ok}` : `导入完成!处理${done},成功${ok}`;
    if (bigGifFallbacks && bigGifFallbacks.length) {
      const lines = bigGifFallbacks.map(f => `- 作品 ${f.id}(约 ${(f.size / 1024 / 1024).toFixed(1)}MB)已自动切换为“保存到本地”,路径:${f.path}`);
      msg += `\n\n以下动图因体积较大,已自动使用本地模式保存(未导入 Eagle):\n${lines.join('\n')}\n\n原因:浏览器/油猴在导入超大 GIF 到 Eagle 时,可能触发内部“消息长度超限”(Message length exceeded maximum allowed length),从而导致任务卡住。当前版本通过自动切换本地模式规避此问题。`;
    }
    showToast(msg, 5000);
    bigGifFallbacks = [];
    document.getElementById('pxeMiniProg')?.remove();
  }

  async function importOne(id, mergeGif = false) {
    cancel = false;
    try {
      const info = await illustInfoAndPages(id);

      // 获取已保存的翻译
      const savedTranslations = LS.get('tagTranslations', {});

      // 应用标签过滤(移除引号)
      const cleanExcludeTag = (tag) => {
        return lower(tag.trim().replace(/^["']|["']$/g, ''));
      };
      const excludeTags = (CFG.filters.excludeTags || '')
        .split(',')
        .map(cleanExcludeTag)
        .filter(Boolean);
      const cleanTags = (tags) => {
        if (!excludeTags.length) return tags || [];
        return (tags || []).filter(t => {
          const lowerTag = lower(t);
          return !excludeTags.some(ex => lowerTag === ex);
        });
      };

      // 应用翻译:将标签替换为已保存的翻译(如果存在),然后去重
      const processTagsWithTranslation = (tags) => {
        // 第1步:先过排除规则
        const filtered = cleanTags(tags || []);
        // 第2步:应用翻译
        const translated = filtered.map(t => {
          const trans = savedTranslations[t];
          if (!trans) return t;
          // 兼容旧格式(字符串)和新格式(对象)
          return typeof trans === 'string' ? trans : (trans.translation || t);
        });
        // 第3步:去重(这样可以避免两个不同日文标签翻译成同一个中文标签时出现重复)
        return Array.from(new Set(translated));
      };

      // 统一的标题截断处理
      const truncateTitle = (title) => {
        const safeTitle = sanitize(title || '');
        const baseName = safeTitle || `pixiv_${id}`;
        return baseName.length > 80 ? baseName.slice(0, 80) : baseName;
      };

      const baseCommon = { website: `https://www.pixiv.net/artworks/${id}` };
      const modTime = (CFG.feature.useUploadAsAddDate && info.uploadDate) ? new Date(info.uploadDate).getTime() : undefined;
      const rng = parseRange(CFG.filters.pageRange); const urls = info.pageUrls || [];
      if (CFG.mode === 'eagle') {
        const fid = await ensureArtistFolder(info.userId, info.userName);
        let items = [];
        if (info.illustType === 2) {
          // 生成 GIF:若体积过大则自动切换为本地模式保存
          const blob = await GifHelper.convertToGifBlob(id);
          if (blob.size > BIG_GIF_LIMIT) {
            const safeUser = sanitize(info.userName || ("Pixiv_" + info.userId));
            const baseDir = `Pixiv/${safeUser}_${info.userId}`;
            const savePath = `${baseDir}/${id}.gif`;
            await saveBlobAsWithPath(savePath, blob);
            showToast(`已完成:动图体积约 ${(blob.size / 1024 / 1024).toFixed(1)}MB,已自动切换为“保存到本地”模式并保存到\n${savePath}\n\n原因:浏览器/油猴在导入超大 GIF 到 Eagle 时,可能触发内部“消息长度超限”限制,导致任务卡住。`, 4000);
            return;
          } else {
            const baseName = truncateTitle(info.title);
            const name = `${baseName}_${id}.gif`;
            const dataURL = await blobToDataURL(blob);
            const processedTags = processTagsWithTranslation(info.tags || []);
            const one = { url: dataURL, name: name, tags: Array.from(new Set([...processedTags, info.userName].filter(Boolean))) };
            if (modTime) one.modificationTime = modTime;
            items.push({ ...baseCommon, ...one });
          }
        } else {
          let use = urls; if (rng) use = urls.filter((_, i) => { const p = i + 1; return p >= rng[0] && p <= rng[1]; }); let i = 0;
          const baseName = truncateTitle(info.title);
          const processedTags = processTagsWithTranslation(info.tags || []);
          items = use.map(u => {
            const itemName = use.length > 1 ? `${baseName}_p${++i}` : baseName;
            const one = { url: u, name: itemName, tags: Array.from(new Set([...processedTags, info.userName].filter(Boolean))), headers: { referer: 'https://www.pixiv.net/' } };
            if (modTime) one.modificationTime = modTime;
            return { ...baseCommon, ...one };
          });
        }
        if (items.length) { await addToEagle(items, fid); }
        showToast('已完成:已发送到 Eagle' + (info.illustType === 2 ? '(GIF 已导入)' : ''));
      } else {
        // Disk 模式:保存到 Downloads/Pixiv/作者ID/ 目录
        const safeUser = sanitize(info.userName || ("Pixiv_" + info.userId));
        const baseDir = `Pixiv/${safeUser}_${info.userId}`;
        if (info.illustType === 2) {
          const savePath = `${baseDir}/${id}.gif`;
          await GifHelper.saveAndGetDataURL(id, info.title, { saveLocal: true, savePath, needDataURL: false });
        } else {
          let use = urls; if (rng) use = urls.filter((_, i) => { const p = i + 1; return p >= rng[0] && p <= rng[1]; });
          const total = use.length || 1;
          for (let i = 0; i < use.length; i++) {
            const u = use[i]; const ext = inferExtFromUrl(u);
            const fname = total > 1 ? `${baseDir}/${id}_${fmtIndex(i + 1, total)}${ext}` : `${baseDir}/${id}${ext}`;
            await gmDownloadWithHeaders(u, fname, { referer: 'https://www.pixiv.net/' });
          }
        }
        showToast(`已完成:已保存到本地 ${baseDir}`);
      }
    } catch (e) { showToast('发送/下载失败:' + (e && e.message || e), 4000); }
  }


  /******************** 作者文件夹 ********************/
  async function ensureArtistFolder(uid, userName, parentId = null) {
    // 根据作者 uid / 名称在 Eagle 中找到或创建对应文件夹,并写入 pid 备注
    const folders = await listFolders();
    const all = flattenFolders(folders);

    const hasUid = uid !== undefined && uid !== null && uid !== '';
    const uidStr = hasUid ? String(uid) : '';

    if (hasUid) {
      const pidRe = /pid\s*=\s*(\d+)/;
      const hit = all.find(f => {
        const m = (f.description || '').match(pidRe);
        return m && m[1] === uidStr;
      });
      if (hit) return hit.id;
    }

    const safe = sanitize(
      userName || (hasUid ? ('Pixiv_' + uidStr) : 'Pixiv_Unknown')
    );

    const same = all.find(f => (f.folderName || f.name) === safe);
    if (same) {
      if (hasUid) {
        try { await updateFolderDesc(same.id, `pid = ${uidStr}`); } catch { }
      }
      return same.id;
    }

    const id = await createFolder(safe, parentId);
    if (hasUid) {
      try { await updateFolderDesc(id, `pid = ${uidStr}`); } catch { }
    }
    return id;
  }


  /******************** 勾选框(同 0.9.5.4) ********************/
  let lastChecked = null;
  function addCheck(a) {
    const m = a.href.match(/artworks\/(\d+)/); if (!m) return;
    const id = m[1];
    if (document.querySelector(`.pxe-mini-checkbox[data-id="${id}"]`)) return;
    let host = a.closest('div[role="listitem"], div[data-testid], figure, li, article, a');
    if (!host) host = a.parentElement || a;
    function findPositionedAncestor(el) {
      let p = el;
      while (p && p !== document.body) {
        const pos = getComputedStyle(p).position;
        if (pos && pos !== 'static') return p;
        p = p.parentElement;
      }
      return null;
    }
    const container = findPositionedAncestor(host) || host;
    const cb = document.createElement('input');
    cb.type = 'checkbox';
    cb.className = 'pxe-mini-checkbox';
    cb.dataset.id = id;
    Object.assign(cb.style, {
      position: 'absolute', top: '6px', left: '6px', zIndex: 2147483001,
      width: '18px', height: '18px', accentColor: '#409EFF', cursor: 'pointer'
    });
    cb.addEventListener('click', (e) => {
      e.stopPropagation();
      if (e.shiftKey && lastChecked) {
        const all = Array.from(new Map(Array.from(document.querySelectorAll('.pxe-mini-checkbox')).map(x => [x.dataset.id, x])).values());
        const i1 = all.indexOf(lastChecked), i2 = all.indexOf(cb);
        const [s, e2] = [Math.min(i1, i2), Math.max(i1, i2)];
        for (let i = s; i <= e2; i++) all[i].checked = cb.checked;
      }
      lastChecked = cb.checked ? cb : null;
    });
    container.appendChild(cb);
  }
  function scan() { document.querySelectorAll('a[href*="/artworks/"]:not([data-pxe-mini])').forEach(a => { a.dataset.pxeMini = 1; addCheck(a); }); }
  function watch() { scan(); if (!watch._mo) { watch._mo = new MutationObserver(m => { if (m.some(x => x.addedNodes.length)) scan(); }); watch._mo.observe(document.body, { childList: true, subtree: true }); } }

  /******************** 进度条盒子 & UI ********************/
  let cancel = false, t0 = 0, bigGifFallbacks = [];

  function box(id, title) {
    const w = document.createElement('div'); w.id = id; Object.assign(w.style, { position: 'fixed', top: '14px', right: '14px', zIndex: 2147483000 });
    w.innerHTML = `<div style="width:334px;padding:8px;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,.18);background:#fff;font-size:12px;">
    <div style="display:flex;gap:8px;align-items:center;margin-bottom:6px;">
      <div id="${id}-left" style="display:flex;align-items:center;gap:6px;">
        <div style="font-weight:600;color:#333;white-space:nowrap;">${title}</div>
        <button id="${id}-led" title="检查 Eagle (点击重载工具条)" style="border:none;background:transparent;padding:0;cursor:pointer;line-height:1;">●</button>
      </div>
      <div id="${id}-eta" style="margin-left:6px;color:#888;font-size:12px;"></div>
      <button id="${id}-close" style="margin-left:auto;padding:2px 6px;border:none;background:#909399;color:#fff;border-radius:4px;cursor:pointer;">关闭</button>
    </div>
    <div style="flex:1;border:1px solid #e6e6e6;height:16px;border-radius:4px;overflow:hidden;background:#f5f7fa;margin-bottom:6px;">
      <div id="${id}-bar" style="width:0%;height:100%;background:#409eff;color:#fff;text-align:center;line-height:16px;">0%</div>
    </div>
    <div style="display:flex;gap:8px;align-items:center;">
      <div id="${id}-txt" style="color:#666;"></div>
      <button id="${id}-cancel" style="margin-left:auto;padding:2px 6px;border:none;background:#f56c6c;color:#fff;border-radius:4px;cursor:pointer;">取消</button>
    </div>
  </div>`;
    document.body.appendChild(w);
    w.querySelector(`#${id}-close`).onclick = () => w.remove();
    w.querySelector(`#${id}-cancel`).onclick = () => { if (cancel) return; cancel = true; cancelInflight(); const b = w.querySelector(`#${id}-bar`); b.style.background = '#f56c6c'; b.textContent = '取消中...'; };
    w.querySelector(`#${id}-led`).onclick = () => { document.getElementById('pxeMiniBar')?.remove(); setTimeout(mountBar, 0); checkEagleLed(w.querySelector(`#${id}-led`)); };
    checkEagleLed(w.querySelector(`#${id}-led`));
    return w;
  }

  // 统一的进度更新函数(避免 updScan / updImport 重复)
  function updateProgress(boxId, { done = 0, total = 0, ok = 0, collectPhase = false } = {}) {
    const b = document.querySelector(`#${boxId}-bar`);
    const t = document.querySelector(`#${boxId}-txt`);
    const e = document.querySelector(`#${boxId}-eta`);

    if (collectPhase) {
      if (b) { b.style.width = '0%'; b.textContent = '收集中'; }
      if (t) { t.textContent = `已找到 ${done} 个作品ID`; }
      return;
    }

    const p = total > 0 ? Math.round(done / total * 100) : 0;
    if (b) { b.style.width = Math.max(1, p) + '%'; b.textContent = `${p}%`; }
    if (t) { t.textContent = `${done} / ${total} 作品 (成功:${ok})`; }

    const dt = (Date.now() - t0) / 1000;
    // 需要至少 1 秒且 done >= 1 才能计算速度
    if (dt >= 1 && done > 0) {
      const rate = done / dt;
      const remain = total - done;
      const eta = rate > 0 ? Math.round(remain / rate) : 0;
      if (e) { e.textContent = `ETA ${Math.floor(eta / 60)}m${eta % 60}s`; }
    } else if (e) {
      // 数据不足,显示占位符
      e.textContent = '计算中...';
    }
  }

  function showScan() { cancel = false; t0 = Date.now(); document.getElementById('pxeScan')?.remove(); const el = box('pxeScan', '扫描作品'); el.querySelector('#pxeScan-txt').textContent = '正在收集作品ID...'; updateProgress('pxeScan', { done: 0, total: 0, collectPhase: true }); }
  function closeScan() { document.getElementById('pxeScan')?.remove(); }
  function showImport(total) { cancel = false; t0 = Date.now(); document.getElementById('pxeMiniProg')?.remove(); const el = box('pxeMiniProg', 'PTE'); el.querySelector('#pxeMiniProg-txt').textContent = `0 / ${total} 作品`; }

  // 向后兼容的旧函数(现在委托给 updateProgress)
  function updScan(done, total, collectPhase) {
    updateProgress('pxeScan', { done, total, collectPhase });
  }
  function updImport(done, total, ok = 0) {
    updateProgress('pxeMiniProg', { done, total, ok });
    if (done === total && !cancel) { setTimeout(() => document.getElementById('pxeMiniProg')?.remove(), 1200); }
  }

  /******************** Eagle 连接指示 ********************/
  async function checkEagle() { try { const r = await xhr({ url: EAGLE.base + EAGLE.api.list }); return !!(r && (r.data || r.folders)); } catch { return false; } }
  async function checkEagleLed(el) {
    const ok = await checkEagle();
    if (!el) return ok;
    el.textContent = '●';
    el.style.color = ok ? '#10b981' : '#ef4444';
    el.title = (ok ? 'Eagle 已连接' : 'Eagle 未连接') + '(点击重载工具条)';
    return ok;
  }

  /******************** 收集ID ********************/
  function collectIdsFromPage() {
    const anchors = Array.from(document.querySelectorAll('a[href*="/artworks/"]'));
    return [...new Set(anchors.map(a => a.href.match(/artworks\/(\d+)/)?.[1]).filter(Boolean))];
  }

  /******************** 极简长条 UI(保持 0.9.5.4) ********************/
  function isCollapsed() { return !!LS.get('collapsed', false); }
  function setCollapsed(v, pos) {
    LS.set('collapsed', !!v);
    const bar = document.getElementById('pxeMiniBar');
    if (!v) {
      // 还原:优先用当前小圆点中心作为 anchor
      if (bar) {
        try {
          const r = bar.getBoundingClientRect();
          const anchor = { x: Math.round(r.left + r.width / 2), y: Math.round(r.top + r.height / 2) };
          localStorage.setItem(LSKEY + ':anchor', JSON.stringify(anchor));
          // 同时把当前左上角写回 barPos,作为还原时的基准
          LS.set('barPos', { x: Math.round(r.left), y: Math.round(r.top) });
        } catch { }
        bar.remove();
      }
      // 重新挂载为面板
      mountBar();
      return;
    } else {
      // 缩小:允许传入目标左上角 pos(来自缩小按钮计算),否则保留现有 barPos
      if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
        LS.set('barPos', { x: Math.floor(pos.x), y: Math.floor(pos.y) });
      }
      if (bar) bar.remove();
      mountBar();
      return;
    }
  }
  function enableCollapsedDragOrClick(bar, m) {
    let dragging = false, moved = false, sx = 0, sy = 0;

    function clamp(x, y, w, h) {
      const nx = Math.min(window.innerWidth - m - w, Math.max(m, x));
      const ny = Math.min(window.innerHeight - m - h, Math.max(m, y));
      return { x: nx, y: ny };
    }

    bar.addEventListener('pointerdown', (ev) => {
      dragging = true; moved = false; sx = ev.clientX; sy = ev.clientY;
      try { bar.setPointerCapture(ev.pointerId); } catch { }
      bar.style.cursor = 'grabbing';
    });

    bar.addEventListener('pointermove', (ev) => {
      if (!dragging) return;
      const dx = ev.clientX - sx, dy = ev.clientY - sy;
      if (Math.abs(dx) + Math.abs(dy) > 3) moved = true;
      const r = bar.getBoundingClientRect();
      const w = r.width, h = r.height;
      const pos = clamp(r.left + dx, r.top + dy, w, h);
      bar.style.left = pos.x + 'px';
      bar.style.top = pos.y + 'px';
      sx = ev.clientX; sy = ev.clientY;
    });

    function finish(ev) {
      if (!dragging) return;
      dragging = false; bar.style.cursor = 'grab';
      try {
        const r = bar.getBoundingClientRect();
        localStorage.setItem(LSKEY + ':barPos', JSON.stringify({ x: Math.round(r.left), y: Math.round(r.top) }));
      } catch { }
      if (!moved) {
        // 视为点击:展开面板
        setCollapsed(false);
      }
      try { bar.releasePointerCapture(ev.pointerId); } catch { }
    }

    bar.addEventListener('pointerup', finish);
    bar.addEventListener('pointercancel', finish);
  }

  /** 拖动整块面板(非最小化状态)。handleEl 存在时,只允许拖动 handleEl 区域 */
  function enableDrag(box, margin, handleEl) {
    const target = handleEl || box;
    let dragging = false, sx = 0, sy = 0;

    function clamp(x, y, w, h) {
      const nx = Math.min(window.innerWidth - margin - w, Math.max(margin, x));
      const ny = Math.min(window.innerHeight - margin - h, Math.max(margin, y));
      return { x: nx, y: ny };
    }

    target.addEventListener('pointerdown', (ev) => {
      // 只允许左键 / 触摸
      if (ev.button !== undefined && ev.button !== 0) return;
      dragging = true;
      try { target.setPointerCapture(ev.pointerId); } catch { }
      const r = box.getBoundingClientRect();
      sx = ev.clientX - r.left;
      sy = ev.clientY - r.top;
      document.body.style.userSelect = 'none';
    });

    target.addEventListener('pointermove', (ev) => {
      if (!dragging) return;
      const r = box.getBoundingClientRect();
      const { x, y } = clamp(ev.clientX - sx, ev.clientY - sy, r.width, r.height);
      box.style.left = x + 'px';
      box.style.top = y + 'px';
    });

    function finish(ev) {
      if (!dragging) return;
      dragging = false;
      try { target.releasePointerCapture(ev.pointerId); } catch { }
      document.body.style.userSelect = '';
      try {
        const r = box.getBoundingClientRect();
        localStorage.setItem(LSKEY + ':barPos', JSON.stringify({ x: Math.round(r.left), y: Math.round(r.top) }));
      } catch { }
    }
    target.addEventListener('pointerup', finish);
    target.addEventListener('pointercancel', finish);
  }

  function mountBar() {
    if (document.getElementById('pxeMiniBar')) return;
    const m = CFG.ui.margin; const pos = LS.get('barPos', { x: CFG.ui.x, y: CFG.ui.y });
    const bar = document.createElement('div'); bar.id = 'pxeMiniBar'; document.body.appendChild(bar);

    const colW = 32, gapX = 10, pad = 10, cols = 3;
    const fixedW = cols * colW + (cols - 1) * gapX + pad * 2;

    if (isCollapsed()) {
      Object.assign(bar.style, {
        position: 'fixed', zIndex: 2147483647, left: pos.x + 'px', top: pos.y + 'px',
        width: '40px', height: '40px', borderRadius: '999px', background: '#409eff',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        color: '#fff', fontWeight: '700', fontSize: '16px',
        boxShadow: '0 6px 22px rgba(0,0,0,.12)', userSelect: 'none', cursor: 'grab'
      });
      bar.style.background = (CFG.mode === 'disk' ? COLOR.disk : COLOR.eagle);
      bar.textContent = (CFG.mode === 'disk' ? 'D' : 'E');
      bar.title = '展开 (单击) / 拖动 (移动位置)';
      enableCollapsedDragOrClick(bar, m);
      return;
    }

    Object.assign(bar.style, {
      position: 'fixed', zIndex: 2147483647, left: pos.x + 'px', top: pos.y + 'px',
      background: 'rgba(255,255,255,0.96)', border: '1px solid rgba(0,0,0,.08)', borderRadius: '12px',
      boxShadow: '0 6px 22px rgba(0,0,0,.12)', boxSizing: 'border-box',
      padding: `8px ${pad}px`, overflow: 'hidden', userSelect: 'none',
      width: fixedW + 'px', maxWidth: `calc(100vw - ${m * 2}px)`
    });

    // 顶部:标题(蓝色粗体 PTE) + 绿灯 + 时钟 + D/E + 缩小
    const topRow = document.createElement('div');
    Object.assign(topRow.style, { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' });

    const title = document.createElement('div'); title.textContent = 'PTE';
    title.style.cssText = 'font-size:12px;cursor:move;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:700;color:#1f6fff;flex-shrink:0;';

    // 中间容器:绿灯、时钟、D/E 平均分配
    const middleGroup = document.createElement('div');
    Object.assign(middleGroup.style, { display: 'flex', alignItems: 'center', gap: '4px', flex: '1', justifyContent: 'space-around', minWidth: '0' });

    const led = document.createElement('button'); led.textContent = '●'; led.title = '检查 Eagle (点击重载工具条)';
    led.style.cssText = 'border:none;background:transparent;padding:0;cursor:pointer;line-height:1;color:#10b981;font-size:12px;flex:0 1 auto;';
    led.onclick = () => { const r = bar.getBoundingClientRect(); LS.set('barPos', { x: r.left, y: r.top }); bar.remove(); setTimeout(mountBar, 0); };
    checkEagleLed(led);



    // 顶部模式指示:显示 'E' 或 'D',仅用字体颜色区分;点击可切换
    const modeMark = document.createElement('button'); modeMark.setAttribute('data-pxe-mode-mark', '1');
    function updateModeMark() {
      const disk = (CFG.mode === 'disk');
      modeMark.textContent = disk ? 'D' : 'E';
      modeMark.title = disk ? '本地模式(点击切换)' : 'Eagle 模式(点击切换)';
      modeMark.style.cssText = 'border:none;background:transparent;padding:0;width:16px;height:18px;'
        + 'font-size:12px;font-weight:700;cursor:pointer;line-height:18px;text-align:center;flex:0 1 auto;'
        + 'color:' + (disk ? COLOR.disk : COLOR.eagle) + ';';
    }
    updateModeMark();
    modeMark.onclick = () => { CFG.mode = (CFG.mode === 'disk' ? 'eagle' : 'disk'); try { LS.set('mode', CFG.mode); } catch { } updateModeMark(); render(); };

    // 顶部时钟(仅在开启时显示;点击即可关闭并消失)
    const topClockBox = document.createElement('span');

    function updateTopClock() {
      // 顶部工具条始终显示“投稿时间→添加日期”开关
      const on = !!CFG.feature.useUploadAsAddDate;
      try {
        topClockBox.style.display = 'inline-block';
        topClockBox.textContent = '🕒';
        topClockBox.title = on ? '投稿时间→添加日期:已启用(点击关闭)' : '投稿时间→添加日期:未启用(点击开启)';
        topClockBox.style.cssText = [
          'cursor:pointer',
          'font-size:12px',
          'line-height:1',
          'padding:0 2px',
          'flex:0 1 auto',
          on ? 'filter:none' : 'filter:grayscale(100%) opacity(0.55)'
        ].join(';');
        topClockBox.onclick = () => {
          CFG.feature.useUploadAsAddDate = !CFG.feature.useUploadAsAddDate;
          try { LS.set('useUploadAsAddDate', CFG.feature.useUploadAsAddDate); } catch (e) { }
          updateTopClock();
          try { render && render(); } catch (e) { }
        };
      } catch (e) {
        // 若顶部容器不存在,降级隐藏
        try {
          topClockBox.style.display = 'none';
          topClockBox.textContent = '';
          topClockBox.removeAttribute('title');
          topClockBox.onclick = null;
        } catch (_) { }
      }
    }
    // 初始渲染一次
    updateTopClock();
    // 监听 LS.set,以便在第二页切换时同步顶部图标
    try {
      const _LSset = LS.set.bind(LS);
      LS.set = (k, v) => {
        _LSset(k, v);
        if (k === 'useUploadAsAddDate') { try { updateTopClock(); } catch (e) { } }
      };
    } catch (e) { }

    const shrink = document.createElement('button'); shrink.textContent = '➖'; shrink.title = '缩小';
    shrink.style.cssText = 'padding:0 4px;height:20px;border:none;background:transparent;color:#6b7280;border-radius:4px;cursor:pointer;font-size:16px;flex-shrink:0;';
    shrink.onclick = () => {
      const sr = shrink.getBoundingClientRect();
      const size = 40; const m = CFG.ui.margin;
      let x = sr.right - size; let y = sr.top - Math.max(0, (size - sr.height) / 2);
      x = Math.min(window.innerWidth - m - size, Math.max(m, x));
      y = Math.min(window.innerHeight - m - size, Math.max(m, y));
      try { localStorage.setItem(LSKEY + ':anchor', JSON.stringify({ x: x + size / 2, y: y + size / 2 })); } catch { }
      setCollapsed(true, { x: Math.floor(x), y: Math.floor(y) });
    };

    middleGroup.append(led, topClockBox, modeMark);
    topRow.append(title, middleGroup, shrink);
    bar.appendChild(topRow);
    // 用 anchor(小圆点中心) 来精确对齐缩小按钮:
    // 计算缩小按钮相对整个面板的中心偏移,然后把面板左上角设置为 anchor - 偏移
    try {
      const anchorRaw = localStorage.getItem(LSKEY + ':anchor');
      if (anchorRaw) {
        const anchor = JSON.parse(anchorRaw);
        const br = bar.getBoundingClientRect();
        const sr = shrink.getBoundingClientRect();
        const relX = (sr.left - br.left) + sr.width / 2;
        const relY = (sr.top - br.top) + sr.height / 2;
        let nx = Math.round(anchor.x - relX);
        let ny = Math.round(anchor.y - relY);
        const m = CFG.ui.margin;
        const vw = window.innerWidth, vh = window.innerHeight;
        // 夹取,保证面板完整可见
        nx = Math.max(m, Math.min(vw - m - br.width, nx));
        ny = Math.max(m, Math.min(vh - m - br.height, ny));
        bar.style.left = nx + 'px';
        bar.style.top = ny + 'px';
        try { localStorage.setItem(LSKEY + ':barPos', JSON.stringify({ x: nx, y: ny })); } catch { }
        try { localStorage.removeItem(LSKEY + ':anchor'); } catch { }
      }
    } catch { }
    // 网格按钮
    const grid = document.createElement('div');
    Object.assign(grid.style, { display: 'grid', gridTemplateColumns: 'repeat(3, 32px)', justifyContent: 'start', justifyItems: 'center', gap: '6px 10px', alignItems: 'center' });
    bar.appendChild(grid);
    grid.style.gridAutoRows = '28px';


    // 统一按钮尺寸 & 顶部模式同步
    const BTN = 40; // 与第一页一致(如需调整,改这里即可)
    function syncModeMark() {
      const el = document.querySelector('[data-pxe-mode-mark="1"]');
      if (!el) return;
      const disk = (CFG.mode === 'disk');
      el.textContent = disk ? 'D' : 'E';
      el.title = disk ? '本地模式(点击切换)' : 'Eagle 模式(点击切换)';
      el.style.color = disk ? COLOR.disk : COLOR.eagle;
    }


    function iconBtn(emoji, tip, onClick, opts = {}) {
      const b = document.createElement('button'); b.textContent = emoji; b.title = tip;
      const bg = opts.bg || '#409eff';
      b.style.cssText = `width:32px;height:28px;margin:0;box-sizing:border-box;padding:0;border:none;background:${bg};border-radius:8px;box-shadow:0 1px 2px rgba(0,0,0,.06);cursor:pointer;font-size:16px;line-height:28px;text-align:center;text-align:center;text-align:center;text-align:center;text-align:center;`;
      b.onclick = onClick; return b;
    }
    function spacer() { const b = document.createElement('button'); b.title = ''; b.disabled = true; b.style.cssText = `width:${BTN}px;height:${BTN}px;padding:0;border:none;background:transparent;border-radius:8px;opacity:0;pointer-events:none;`; return b; }
    function invertSelection() { document.querySelectorAll('.pxe-mini-checkbox').forEach(cb => { cb.checked = !cb.checked; }); }

    const onArtwork = isArtwork();
    const onUserPage = isUser();
    const state = { page: 1 };

    const render = () => {
      grid.innerHTML = '';
      if (state.page === 1) {
        if (onUserPage) {
          grid.append(
            iconBtn('🌐', '作者全部', () => importMode('all')),
            iconBtn('📄', '本页', () => importMode('page')),
            iconBtn('✅', '仅勾选', () => importMode('selected')),
            iconBtn('☑️', '全选', () => { document.querySelectorAll('.pxe-mini-checkbox').forEach(cb => { cb.checked = true; }); }),
            iconBtn('◻️', '全不选', () => { document.querySelectorAll('.pxe-mini-checkbox').forEach(cb => { cb.checked = false; }); }),
            iconBtn('➡️', '下一页', () => { state.page = 2; render(); })
          );
        } else if (onArtwork) {
          // 详情页:六键布局,顺序:此作 | 本页 | 仅勾选 | 全选 | 全不选 | 下一页
          grid.append(
            iconBtn('🎯', '此作', () => importMode('one')),
            iconBtn('📄', '本页', () => importMode('page')),
            iconBtn('✅', '仅勾选', () => importMode('selected')),
            iconBtn('☑️', '全选', () => { document.querySelectorAll('.pxe-mini-checkbox').forEach(cb => { cb.checked = true; }); }),
            iconBtn('◻️', '全不选', () => { document.querySelectorAll('.pxe-mini-checkbox').forEach(cb => { cb.checked = false; }); }),
            iconBtn('➡️', '下一页', () => { state.page = 2; render(); })
          );
        } else {
          grid.append(
            iconBtn('🌐', '本页全部', () => importMode('page')),
            iconBtn('📄', '本页', () => importMode('page')),
            iconBtn('✅', '仅勾选', () => importMode('selected')),
            iconBtn('☑️', '全选', () => { document.querySelectorAll('.pxe-mini-checkbox').forEach(cb => { cb.checked = true; }); }),
            iconBtn('◻️', '全不选', () => { document.querySelectorAll('.pxe-mini-checkbox').forEach(cb => { cb.checked = false; }); }),
            iconBtn('➡️', '下一页', () => { state.page = 2; render(); })
          );
        }
      } else {
        // 第二页:反选 + 模式切换(E/D) + 选择下载目录 + 公告按钮 + 上一页(已将“投稿时间→添加日期”移动到顶部工具条)

        const btnInvert = iconBtn('🔁', '反选', invertSelection);
        const btnTagManager = iconBtn('🏷️', '标签管理', async () => { await createTagManagerModal(); });
        const btnPick = iconBtn('📁', '选择下载目录', async () => { await ptePickDownloadsRoot(); }, { bg: '#f1a72e' });
        const btnNotice = iconBtn('📜', '公告', () => { createWelcomeModal(Date.now()); });
        const btnBack = iconBtn('⬅️', '上一页', () => { state.page = 1; render(); });
        try {
          grid.style.gridTemplateColumns = 'repeat(3, 32px)';
          btnTagManager.style.gridColumn = '2';
          btnTagManager.style.gridRow = '1';
          btnPick.style.gridColumn = '1';
          btnPick.style.gridRow = '2';
          btnNotice.style.gridColumn = '2';
          btnNotice.style.gridRow = '2';
          btnBack.style.gridColumn = '3';
          btnBack.style.gridRow = '2';
        } catch (e) { }
        grid.append(btnInvert, btnTagManager, spacer(), btnPick, btnNotice, btnBack);
      }
    };

    render();
    enableDrag(bar, m, title);
  }

  watch();
  setTimeout(mountBar, 0);

  try {
    // Dynamic update date from script install time (YYYY-MM-DD)
    var PTE_UPDATED_DATE = '2025-11-19';
    try {
      if (typeof GM_info !== 'undefined' && GM_info && GM_info.script && GM_info.script.installed) {
        PTE_UPDATED_DATE = new Date(GM_info.script.installed).toISOString().split('T')[0];
      }
    } catch (e) { }

    // Use existing LS helper if available; otherwise namespaced localStorage shim
    var _LS = (typeof LS !== 'undefined' && LS && typeof LS.get === 'function')
      ? LS
      : {
        get: function (k, d) {
          try {
            var v = localStorage.getItem('pxeMini:' + k);
            return v !== null ? JSON.parse(v) : d;
          } catch (e) { return d; }
        },
        set: function (k, v) {
          try {
            localStorage.setItem('pxeMini:' + k, JSON.stringify(v));
          } catch (e) { }
        }
      };

    function fmtTime(ts) {
      try {
        return new Date(ts).toLocaleString('zh-CN', { hour12: false });
      } catch (e) {
        return '' + ts;
      }
    }

    function createWelcomeModal(updatedAtTs) {
      if (document.getElementById('pteWelcome')) return;
      var mask = document.createElement('div');
      mask.id = 'pteWelcome';
      Object.assign(mask.style, {
        position: 'fixed', inset: '0',
        background: 'rgba(0,0,0,.35)',
        backdropFilter: 'blur(2px)',
        zIndex: 2147483647,
        display: 'flex', alignItems: 'center', justifyContent: 'center'
      });
      var box = document.createElement('div');
      Object.assign(box.style, {
        width: 'min(560px,92vw)',
        borderRadius: '16px',
        background: '#fff',
        boxShadow: '0 12px 40px rgba(0,0,0,.18)',
        padding: '16px 18px',
        fontSize: '13px',
        color: '#444',
        lineHeight: '1.6',
        maxHeight: '80vh', overflow: 'auto'
      });
      var timeStr = PTE_UPDATED_DATE;
      box.innerHTML = ''
        + '<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;">'
        + '<div style="font-size:18px;font-weight:700;color:#1f6fff;">PTE 已更新 ✅</div>'
        + '<span style="margin-left:auto;color:#999;font-size:12px">v' + PTE_VER + '</span>'
        + '</div>'
        + '<div style="color:#999;font-size:12px;margin-bottom:8px;">更新时间:' + timeStr + ' | 版本号:v' + PTE_VER + '</div>'
        + '<div>'
        + '<p>右上角工具条:<b style="color:#409eff">E(蓝)</b> = Eagle 模式,<b style="color:#f1a72e">D(橙)</b> = 本地模式。</p>'
        + '<p>详情页六键:<code>此作</code> / <code>本页</code> / <code>仅勾选</code> / <code>全选</code> / <code>全不选</code> / <code>下一页</code>。</p><p>顶部工具条新增并固定“🕒 投稿时间→添加日期”开关(点击切换;关闭时灰度显示)。</p>'
        + '<p>第二页:🔁 反选 · 📁 选择下载目录(左下) · 📜 公告 · ⬅️ 上一页(右下)。</p>'
        + '<p><b style="color:#ff4d4f">大动图说明:</b> 当 ugoira→GIF 体积过大(约 &gt;40MB)时,脚本会自动从 Eagle 模式切换为“保存到本地”模式,并保存到下载目录下的 <code>Pixiv/作者名_作者ID/作品ID.gif</code>,以避免浏览器 / 油猴在导入 Eagle 时因消息过长而卡住。</p>'
        + '<p style="color:#666">小技巧:点击绿灯检查 Eagle;点“➖”可缩小为悬浮圆点。</p>'
        + '<p style="margin-top:6px"><b>没看到弹窗/工具条?</b> 如果脚本已启动但首次没看到,UI 可能在浏览器窗口右侧;请尝试将浏览器窗口<b>拉宽</b>即可看见。</p>'
        + '<p><b>连续多选:</b> 在列表/缩略图页,先点击左侧的勾选框选中一项,然后按住 <kbd>Shift</kbd> 再点击另一项,<b>两者之间的范围</b>会被一次性选中。</p>'
        + '</div>'
        + '<div style="display:flex;gap:10px;margin-top:14px;justify-content:flex-end;">'
        + '<button id="pxeWelcomeOk" style="padding:6px 14px;border:none;border-radius:8px;background:#409eff;color:#fff;cursor:pointer;font-weight:600">我知道了</button>'
        + '</div>';
      mask.appendChild(box);
      document.body.appendChild(mask);
      mask.addEventListener('click', function (e) { if (e.target === mask) mask.remove(); });
      var ok = box.querySelector('#pxeWelcomeOk');
      if (ok) ok.addEventListener('click', function () { mask.remove(); });
    }

    function showWelcomePerVersion() {
      var hasShown = _LS.get('welcomeShownOnce', false);

      // Only show welcome modal once in the user's lifetime
      if (!hasShown) {
        var now = Date.now();
        _LS.set('welcomeAt', now);
        _LS.set('welcomeVer', PTE_VER);
        _LS.set('welcomeShownOnce', true);
        // Show after DOM ready
        if (document.readyState === 'loading') {
          document.addEventListener('DOMContentLoaded', function () { setTimeout(function () { createWelcomeModal(now); }, 200); }, { once: true });
        } else {
          setTimeout(function () { createWelcomeModal(now); }, 200);
        }
      }
    }

    // Schedule after the script's own UI mounts; using a slight delay avoids racing existing layout code
    setTimeout(showWelcomePerVersion, 600);
  } catch (e) { /* silent */ }
})();
/* === /PTE Welcome Modal (auto-insert) === */