☰

Danbooru Prompt Extractor

Danbooru提示词提取器 - ⚙️设置(自选图标/过滤规则NAI·Anima·自定义/通配符匹配)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Danbooru Prompt Extractor
// @namespace    danbooru-prompt-extractor
// @version      1.1.1
// @description  Danbooru提示词提取器 - ⚙️设置(自选图标/过滤规则NAI·Anima·自定义/通配符匹配)
// @match        https://danbooru.donmai.us/posts/*
// @match        https://danbooru.donmai.us/posts?*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===== 内置过滤规则预设 =====
    // NAI规则:只过滤马赛克/审查类(保留 pubic_hair 不删)
    var RULE_NAI = ['mosaic', 'mosaic_censoring', 'censored', 'censor', 'censoring',
        'bar_censor', 'black_bar', 'pixelated', 'pixelation',
        'patchwork_censor', 'spotlight_censor', 'blur', 'blurred', 'covering'];
    // Anima规则:审查类 + commentary类(*支持前后缀通配:commentary*=开头,*commentary=结尾)
    var RULE_ANIMA = RULE_NAI.concat(['commentary*', '*commentary', 'translated*',
        'speech_bubble', 'text*', 'high_score_dialogue']);

    // ===== localStorage 状态存储 =====
    function lsGet(k, d) { try { var v = localStorage.getItem('dpe_' + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } }
    function lsSet(k, v) { try { localStorage.setItem('dpe_' + k, JSON.stringify(v)); return true; } catch (e) { return false; } }

    var rule = lsGet('rule', 'nai');           // nai | anima | custom
    var customWords = lsGet('customWords', null); // 自定义词表(初始=Anima复制)
    if (!customWords) { customWords = RULE_ANIMA.slice(); lsSet('customWords', customWords); }
    var icons = lsGet('icons', []);            // [{name, data(base64)}]
    var activeIcon = lsGet('activeIcon', -1);  // -1 = 默认🏷

    function toast(msg) {
        var o = document.getElementById('dpe-toast'); if (o) o.remove();
        var t = document.createElement('div'); t.id = 'dpe-toast'; t.textContent = msg;
        document.body.appendChild(t);
        setTimeout(function() { t.style.opacity = '0'; setTimeout(function() { t.remove(); }, 300); }, 1600);
    }
    function currentWords() { return rule === 'custom' ? customWords : (rule === 'anima' ? RULE_ANIMA : RULE_NAI); }

    var css =
        '#dpe-wrap{position:fixed;left:20px;top:300px;width:50px;height:50px;z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,sans-serif}' +
        '#dpe-toggle{width:50px;height:50px;border-radius:50%;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;display:flex;align-items:center;justify-content:center;font-size:24px;box-shadow:0 4px 16px rgba(102,126,234,0.5);cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;touch-action:none;background-size:cover;background-position:center}' +
        '#dpe-panel{display:none;flex-direction:column;position:fixed;background:#fff;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,0.25);overflow:hidden;z-index:2147483647}' +
        '#dpe-head{padding:10px 12px;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;display:flex;align-items:center;justify-content:space-between;font-size:14px;font-weight:600;flex-shrink:0;gap:6px}' +
        '#dpe-tools{display:flex;gap:4px;align-items:center}' +
        '#dpe-tools button{background:rgba(255,255,255,0.2);border:none;color:#fff;font-size:14px;padding:3px 8px;border-radius:6px;cursor:pointer;line-height:1.4}' +
        '#dpe-tools button.on{background:#ff5252}' +
        '#dpe-close{background:rgba(255,255,255,0.2);border:none;color:#fff;font-size:14px;padding:3px 9px;border-radius:6px;cursor:pointer;line-height:1.4;margin-left:2px}' +
        '#dpe-search{display:none;padding:8px 10px;border-bottom:1px solid #eee;flex-shrink:0}' +
        '#dpe-search input{width:100%;box-sizing:border-box;padding:7px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px;outline:none}' +
        '#dpe-body{flex:1;overflow-y:auto;padding:10px;-webkit-overflow-scrolling:touch;min-height:0;touch-action:pan-y}' +
        '.dpe-cat{margin-bottom:10px}' +
        '.dpe-cat-t{font-size:12px;font-weight:600;color:#888;margin-bottom:5px}' +
        '.dpe-cat-c{background:#eee;color:#666;padding:1px 7px;border-radius:10px;font-size:10px;margin-left:6px}' +
        '.dpe-tgs{display:flex;flex-wrap:wrap;gap:4px}' +
        '.dpe-tg{background:#f3f3f3;color:#333;padding:5px 10px;border-radius:5px;font-size:12px;cursor:pointer;border:1px solid #e8e8e8;user-select:none;-webkit-user-select:none;white-space:nowrap;touch-action:pan-y}' +
        '.dpe-delx{display:inline-block;margin-left:5px;color:#fff;background:#e74c3c;border-radius:50%;width:15px;height:15px;line-height:14px;text-align:center;font-size:10px;font-weight:bold;cursor:pointer;vertical-align:1px}' +
        '.dpe-hl{border:2px solid #ff5252 !important;background:#ffe3e3 !important;box-shadow:0 0 6px rgba(255,60,60,.5) !important}' +
        '.dpe-hl-first{border:3px solid #ff1744 !important;background:#ffd0d0 !important}' +
        '#dpe-foot{padding:10px 14px;border-top:1px solid #eee;display:flex;gap:8px;align-items:stretch;flex-shrink:0}' +
        '#dpe-preview{flex:1;background:#f8f8f8;border:1px solid #e0e0e0;border-radius:6px;padding:6px 8px;font-size:11px;color:#555;max-height:70px;overflow-y:auto;word-break:break-all;-webkit-overflow-scrolling:touch;touch-action:pan-y}' +
        '#dpe-copy{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;padding:8px 14px;border-radius:6px;cursor:pointer;font-size:13px;font-weight:500;white-space:nowrap}' +
        // 设置面板
        '#dpe-settings{display:none;padding:10px 12px;border-bottom:1px solid #eee;flex-shrink:0;max-height:46vh;overflow-y:auto;-webkit-overflow-scrolling:touch;background:#fafafa}' +
        '.dpe-set-t{font-size:12px;font-weight:700;color:#667eea;margin:6px 0 6px}' +
        '.dpe-icons{display:flex;flex-wrap:wrap;gap:8px}' +
        '.dpe-ic{position:relative;width:52px;height:52px;border-radius:10px;border:2px solid #ddd;background-size:cover;background-position:center;background-color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:22px}' +
        '.dpe-ic.active{border-color:#667eea;box-shadow:0 0 8px rgba(102,126,234,.6)}' +
        '.dpe-ic .dpe-icx{position:absolute;top:-6px;right:-6px;width:16px;height:16px;background:#e74c3c;color:#fff;border-radius:50%;font-size:10px;line-height:16px;text-align:center}' +
        '.dpe-ic-add{border:2px dashed #bbb;color:#999}' +
        '.dpe-rules{display:flex;gap:6px;margin-bottom:6px}' +
        '.dpe-rules button{flex:1;padding:6px 0;border:1px solid #ddd;background:#fff;border-radius:8px;font-size:12px;cursor:pointer}' +
        '.dpe-rules button.on{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border-color:transparent}' +
        '.dpe-words{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px}' +
        '.dpe-w{background:#eee;color:#555;padding:3px 8px;border-radius:10px;font-size:11px;display:flex;align-items:center;gap:4px}' +
        '.dpe-w .dpe-wx{color:#e74c3c;font-weight:bold;cursor:pointer}' +
        '.dpe-addrow{display:flex;gap:6px}' +
        '.dpe-addrow input{flex:1;padding:6px 8px;border:1px solid #ddd;border-radius:6px;font-size:12px;outline:none}' +
        '.dpe-addrow button{padding:6px 12px;border:none;border-radius:6px;background:#667eea;color:#fff;font-size:12px;cursor:pointer}' +
        '#dpe-toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.85);color:#fff;padding:10px 20px;border-radius:8px;font-size:14px;z-index:2147483648;pointer-events:none;transition:opacity 0.3s}';

    var st = document.createElement('style');
    st.textContent = css;
    document.head.appendChild(st);

    var wrapper = document.createElement('div');
    wrapper.id = 'dpe-wrap';
    wrapper.innerHTML =
        '<div id="dpe-toggle">🏷</div>' +
        '<div id="dpe-panel">' +
        '<div id="dpe-head"><span>🎨 Danbooru</span><span id="dpe-tools"><button id="dpe-search-btn" title="搜索">🔍</button><button id="dpe-filter-btn" title="按当前规则过滤">🧹</button><button id="dpe-del-btn" title="删除模式">🗑️</button><button id="dpe-ok-btn" title="完成" style="display:none">✓</button><button id="dpe-set-btn" title="设置">⚙️</button><button id="dpe-refresh" title="刷新">🔄</button><button id="dpe-close" title="关闭">✕</button></span></div>' +
        '<div id="dpe-settings">' +
        '<div class="dpe-set-t">🖼 图标(点击切换 / × 删除 / ➕ 添加,GIF保留动画)</div>' +
        '<div class="dpe-icons" id="dpe-icons"></div>' +
        '<input type="file" id="dpe-file" accept="image/*" style="display:none">' +
        '<div class="dpe-set-t">🧹 过滤规则(当前:<span id="dpe-rule-name"></span>)</div>' +
        '<div class="dpe-rules">' +
        '<button data-r="nai">NAI</button><button data-r="anima">Anima</button><button data-r="custom">自定义</button>' +
        '</div>' +
        '<div class="dpe-words" id="dpe-words"></div>' +
        '<div class="dpe-addrow" id="dpe-addrow"><input id="dpe-word-in" placeholder="输入过滤词,如 mosaic_gag"/><button id="dpe-word-add">添加</button></div>' +
        '</div>' +
        '<div id="dpe-search"><input type="text" placeholder="输入关键词搜索标签..."/></div>' +
        '<div id="dpe-body"></div>' +
        '<div id="dpe-foot"><div id="dpe-preview"></div><button id="dpe-copy">📋 复制</button></div>' +
        '</div>';
    document.body.appendChild(wrapper);

    var panel = wrapper.querySelector('#dpe-panel');
    var pbody = wrapper.querySelector('#dpe-body');
    var preview = wrapper.querySelector('#dpe-preview');
    var copyBtn = wrapper.querySelector('#dpe-copy');
    var btn = wrapper.querySelector('#dpe-toggle');
    var refreshBtn = wrapper.querySelector('#dpe-refresh');
    var delBtn = wrapper.querySelector('#dpe-del-btn');
    var okBtn = wrapper.querySelector('#dpe-ok-btn');
    var filterBtn = wrapper.querySelector('#dpe-filter-btn');
    var searchBtn = wrapper.querySelector('#dpe-search-btn');
    var setBtn = wrapper.querySelector('#dpe-set-btn');
    var setPanel = wrapper.querySelector('#dpe-settings');
    var closeBtn = wrapper.querySelector('#dpe-close');
    var searchBox = wrapper.querySelector('#dpe-search');
    var searchInput = wrapper.querySelector('#dpe-search input');
    var iconsBox = wrapper.querySelector('#dpe-icons');
    var fileInput = wrapper.querySelector('#dpe-file');
    var wordsBox = wrapper.querySelector('#dpe-words');
    var addRow = wrapper.querySelector('#dpe-addrow');
    var wordIn = wrapper.querySelector('#dpe-word-in');
    var wordAdd = wrapper.querySelector('#dpe-word-add');
    var ruleNameEl = wrapper.querySelector('#dpe-rule-name');

    var isOpen = false, data = null;
    var deleteMode = false, lastKeyword = '';
    var psx = 0, psy = 0, baseL = 20, baseT = 300, moved = false, dragging = false;
    var touchLock = false;

    // ==== 图标 ====
    function applyMainIcon() {
        if (activeIcon >= 0 && icons[activeIcon]) {
            btn.style.backgroundImage = 'url("' + icons[activeIcon].data + '")';
            btn.textContent = '';
        } else {
            btn.style.backgroundImage = '';
            btn.textContent = '🏷';
        }
    }
    function renderIcons() {
        iconsBox.innerHTML = '';
        var def = document.createElement('div');
        def.className = 'dpe-ic' + (activeIcon === -1 ? ' active' : '');
        def.textContent = '🏷';
        def.title = '默认图标';
        def.addEventListener('click', function() { activeIcon = -1; lsSet('activeIcon', -1); applyMainIcon(); renderIcons(); toast('已恢复默认图标'); });
        iconsBox.appendChild(def);
        icons.forEach(function(ic, i) {
            var d = document.createElement('div');
            d.className = 'dpe-ic' + (i === activeIcon ? ' active' : '');
            d.style.backgroundImage = 'url("' + ic.data + '")';
            var x = document.createElement('span');
            x.className = 'dpe-icx'; x.textContent = '×';
            x.addEventListener('click', function(e) {
                e.stopPropagation();
                icons.splice(i, 1);
                if (activeIcon === i) activeIcon = -1; else if (activeIcon > i) activeIcon--;
                lsSet('icons', icons); lsSet('activeIcon', activeIcon);
                applyMainIcon(); renderIcons(); toast('已删除图标');
            });
            d.appendChild(x);
            d.addEventListener('click', function() {
                activeIcon = i; lsSet('activeIcon', i);
                applyMainIcon(); renderIcons(); toast('已切换图标');
            });
            iconsBox.appendChild(d);
        });
        var add = document.createElement('div');
        add.className = 'dpe-ic dpe-ic-add';
        add.textContent = '➕';
        add.addEventListener('click', function() { fileInput.click(); });
        iconsBox.appendChild(add);
    }
    function compress96(file, cb) {
        var img = new Image();
        img.onload = function() {
            var c = document.createElement('canvas'); c.width = 96; c.height = 96;
            var ctx = c.getContext('2d');
            var s = Math.min(img.width, img.height);
            ctx.drawImage(img, (img.width - s) / 2, (img.height - s) / 2, s, s, 0, 0, 96, 96);
            cb(c.toDataURL('image/png'));
        };
        img.onerror = function() { toast('图片读取失败'); };
        img.src = URL.createObjectURL(file);
    }
    fileInput.addEventListener('change', function() {
        var f = fileInput.files[0]; if (!f) return;
        fileInput.value = '';
        if (f.type === 'image/gif') {
            var r = new FileReader();
            r.onload = function() {
                if (r.result.length > 3 * 1024 * 1024) { toast('GIF太大(>3MB),请先压缩'); return; }
                icons.push({ name: f.name, data: r.result });
                if (!lsSet('icons', icons)) { icons.pop(); toast('存储空间不足'); return; }
                renderIcons(); toast('GIF已添加(保留动画)');
            };
            r.readAsDataURL(f);
        } else {
            compress96(f, function(data) {
                icons.push({ name: f.name, data: data });
                if (!lsSet('icons', icons)) { icons.pop(); toast('存储空间不足'); return; }
                renderIcons(); toast('图标已添加');
            });
        }
    });

    // ==== 过滤规则 UI ====
    function renderWords() {
        var ws = currentWords();
        ruleNameEl.textContent = rule === 'nai' ? 'NAI(保留pubic_hair)' : rule === 'anima' ? 'Anima(含commentary类)' : '自定义';
        wordsBox.innerHTML = '';
        ws.forEach(function(w, i) {
            var s = document.createElement('span');
            s.className = 'dpe-w';
            s.innerHTML = w.replace(/</g, '&lt;') + (rule === 'custom' ? ' <span class="dpe-wx">×</span>' : '');
            if (rule === 'custom') {
                s.querySelector('.dpe-wx').addEventListener('click', function() {
                    customWords.splice(i, 1); lsSet('customWords', customWords); renderWords();
                });
            }
            wordsBox.appendChild(s);
        });
        if (!ws.length) wordsBox.innerHTML = '<span style="font-size:11px;color:#999">词表为空,请添加</span>';
        addRow.style.display = rule === 'custom' ? 'flex' : 'none';
        wrapper.querySelectorAll('.dpe-rules button').forEach(function(b) {
            b.classList.toggle('on', b.getAttribute('data-r') === rule);
        });
    }
    wrapper.querySelectorAll('.dpe-rules button').forEach(function(b) {
        b.addEventListener('click', function() {
            rule = b.getAttribute('data-r');
            lsSet('rule', rule);
            renderWords();
            toast('已切换规则:' + (rule === 'nai' ? 'NAI' : rule === 'anima' ? 'Anima' : '自定义') + ',点🧹生效');
        });
    });
    wordAdd.addEventListener('click', function() {
        var w = wordIn.value.trim().replace(/\s+/g, '_').toLowerCase();
        if (!w) return;
        if (customWords.indexOf(w) > -1) { toast('已存在'); return; }
        customWords.push(w); lsSet('customWords', customWords);
        wordIn.value = ''; renderWords(); toast('已添加: ' + w);
    });

    // ==== 定位/开合 ====
    function setPos(l, t) {
        var vw = window.innerWidth, vh = window.innerHeight;
        l = Math.max(0, Math.min(vw - 50, l));
        t = Math.max(0, Math.min(vh - 50, t));
        wrapper.style.left = l + 'px';
        wrapper.style.top = t + 'px';
    }
    function getL() { return parseInt(wrapper.style.left) || 20; }
    function getT() { return parseInt(wrapper.style.top) || 300; }
    setPos(20, 300);

    function openPanel() {
        var vw = window.innerWidth, vh = window.innerHeight;
        var pw = Math.min(380, vw - 16), ph = Math.min(Math.floor(vh * 0.7), 480);
        var r = wrapper.getBoundingClientRect();
        // 面板改为 fixed 定位:基于按钮实际屏幕位置,且始终夹在可视区内
        var left = Math.min(Math.max(8, r.left), Math.max(8, vw - pw - 8));
        var top;
        if (r.bottom + 8 + ph <= vh) {
            top = r.bottom + 8;                       // 默认在按钮下方
        } else if (r.top - 8 - ph >= 0) {
            top = r.top - 8 - ph;                     // 放不下则整体翻到按钮上方
        } else {
            top = Math.max(8, (vh - ph) / 2);         // 都放不下就垂直居中
        }
        panel.style.width = pw + 'px';
        panel.style.maxHeight = ph + 'px';
        panel.style.left = left + 'px';
        panel.style.top = top + 'px';
        panel.style.right = 'auto';
        panel.style.bottom = 'auto';
        panel.style.display = 'flex';
        if (!data) load();
    }
    function closePanel() { isOpen = false; panel.style.display = 'none'; }
    function togglePanel() { isOpen ? closePanel() : (isOpen = true, openPanel()); }

    // ==== 拖拽 ====
    function dragStart(x, y, target) {
        if (panel.contains(target)) return;
        psx = x; psy = y;
        baseL = getL(); baseT = getT();
        moved = false;
        dragging = true;
    }
    function dragMove(x, y, ev) {
        if (!dragging) return;
        var dx = x - psx, dy = y - psy;
        if (!moved && (Math.abs(dx) > 8 || Math.abs(dy) > 8)) moved = true;
        if (moved) {
            if (ev && ev.cancelable) ev.preventDefault();
            setPos(baseL + dx, baseT + dy);
        }
    }
    function dragEnd() {
        var wasMoved = moved;
        dragging = false; moved = false;
        if (wasMoved && isOpen) openPanel();
    }
    wrapper.addEventListener('touchstart', function(e) {
        touchLock = true;
        dragStart(e.touches[0].clientX, e.touches[0].clientY, e.target);
    }, { passive: true });
    document.addEventListener('touchmove', function(e) {
        if (!dragging) return;
        dragMove(e.touches[0].clientX, e.touches[0].clientY, e);
    }, { passive: false });
    document.addEventListener('touchend', function() {
        dragEnd();
        setTimeout(function() { touchLock = false; }, 350);
    });
    wrapper.addEventListener('mousedown', function(e) {
        if (touchLock) return;
        dragStart(e.clientX, e.clientY, e.target);
    });
    document.addEventListener('mousemove', function(e) {
        if (touchLock || !dragging) return;
        dragMove(e.clientX, e.clientY, null);
    });
    document.addEventListener('mouseup', function() {
        if (touchLock) return;
        dragEnd();
    });

    btn.addEventListener('click', function(e) {
        if (moved) { e.preventDefault(); e.stopPropagation(); return; }
        togglePanel();
    });
    panel.addEventListener('click', function(e) { e.stopPropagation(); });
    document.addEventListener('click', function(e) {
        if (!isOpen) return;
        if (e.target === document.body || e.target === document.documentElement) closePanel();
    });

    // ==== 工具函数 ====
    function getPostId() { var m = window.location.pathname.match(/\/posts\/(\d+)/); return m ? m[1] : null; }
    function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '"'); }
    function fbCopy(text) {
        var ta = document.createElement('textarea');
        ta.value = text; ta.style.cssText = 'position:fixed;opacity:0;top:-100px';
        document.body.appendChild(ta); ta.select();
        document.execCommand('copy'); ta.remove();
        toast('已复制: ' + text);
    }

    async function load() {
        var id = getPostId();
        if (!id) { pbody.innerHTML = '<div style="text-align:center;padding:20px;color:#e74c3c">未获取到帖子ID</div>'; return; }
        pbody.innerHTML = '<div style="text-align:center;padding:20px;color:#999">加载中...</div>';
        var tags;
        try {
            var r = await fetch('https://danbooru.donmai.us/posts/' + id + '.json');
            if (!r.ok) throw 0;
            var d = await r.json(); var p = d.post || d;
            tags = {
                general: (p.tag_string_general || '').split(' ').filter(Boolean),
                artist: (p.tag_string_artist || '').split(' ').filter(Boolean),
                copyright: (p.tag_string_copyright || '').split(' ').filter(Boolean),
                character: (p.tag_string_character || '').split(' ').filter(Boolean),
                meta: (p.tag_string_meta || '').split(' ').filter(Boolean)
            };
        } catch (e) { tags = parseDom(); }
        data = tags; render(tags);
    }

    function parseDom() {
        var t = { general: [], artist: [], copyright: [], character: [], meta: [] };
        document.querySelectorAll('a').forEach(function(a) {
            var tx = a.textContent.trim(), hf = a.getAttribute('href') || '';
            if (!tx || tx.length > 50 || hf.indexOf('/tags/') !== 0) return;
            var p = a.closest('.tag-type-general,.tag-type-artist,.tag-type-copyright,.tag-type-character,.tag-type-meta');
            var c = 'general';
            if (p) {
                if (p.classList.contains('tag-type-artist')) c = 'artist';
                else if (p.classList.contains('tag-type-copyright')) c = 'copyright';
                else if (p.classList.contains('tag-type-character')) c = 'character';
                else if (p.classList.contains('tag-type-meta')) c = 'meta';
            }
            if (t[c].indexOf(tx) === -1) t[c].push(tx);
        });
        return t;
    }

    function removeTag(tag) {
        for (var k in data) {
            var i = data[k].indexOf(tag);
            if (i > -1) { data[k].splice(i, 1); return true; }
        }
        return false;
    }

    // 按当前规则过滤(通配符:commentary*=前缀匹配,*commentary=后缀匹配)
    function wordMatch(tag, w) {
        var star = w.indexOf('*');
        if (star === -1) return tag === w;                 // 精确匹配
        if (star === w.length - 1) return tag.indexOf(w.slice(0, -1)) === 0; // 前缀:xxx*
        if (star === 0) return tag.slice(-w.length + 1) === w.slice(1);      // 后缀:*xxx
        return false;
    }
    function applyFilter() {
        if (!data) return;
        var ws = currentWords(), removed = [];
        for (var k in data) {
            data[k] = data[k].filter(function(tag) {
                var hit = null;
                for (var i = 0; i < ws.length; i++) {
                    if (wordMatch(tag, ws[i])) { hit = ws[i]; break; }
                }
                if (hit) { removed.push(tag + '(' + hit + ')'); return false; }
                return true;
            });
        }
        render(data);
        if (removed.length) toast('[' + rule.toUpperCase() + '] 已过滤 ' + removed.length + ' 个: ' + removed.join(', '));
        else toast('没有需要过滤的标签');
    }

    function doSearch(kw) {
        var tgs = pbody.querySelectorAll('.dpe-tg');
        tgs.forEach(function(el) { el.classList.remove('dpe-hl', 'dpe-hl-first'); });
        if (!kw) return;
        var q = kw.toLowerCase(), first = null;
        tgs.forEach(function(el) {
            var txt = el.childNodes[0] ? el.childNodes[0].textContent : el.textContent;
            if (txt.toLowerCase().indexOf(q) > -1) {
                el.classList.add('dpe-hl');
                if (!first) { first = el; el.classList.add('dpe-hl-first'); }
            }
        });
        if (first) first.scrollIntoView({ block: 'center', behavior: 'smooth' });
    }

    function syncPreview() {
        var sep = ', ';
        var all = [];
        for (var k in data) {
            var arr = data[k].filter(function(x) { return x && x.length > 1; });
            if (arr.length) all.push(arr.join(sep));
        }
        if (preview) preview.textContent = all.join(sep) || '';
    }

    function render(tags) {
        var sep = ', ';
        var names = { general: '通用', artist: '画师', copyright: '版权', character: '角色', meta: '元' };
        var icons2 = { general: '🏷', artist: '🎨', copyright: '©', character: '👤', meta: '⚙' };
        var html = '';
        for (var k in names) {
            var arr = tags[k].filter(function(x) { return x && x.length > 1; });
            if (!arr.length) continue;
            html += '<div class="dpe-cat"><div class="dpe-cat-t">' + icons2[k] + ' ' + names[k] + ' <span class="dpe-cat-c">' + arr.length + '</span></div><div class="dpe-tgs">' +
                arr.map(function(x) {
                    return '<span class="dpe-tg" data-v="' + esc(x) + '">' + esc(x) + (deleteMode ? '<span class="dpe-delx">×</span>' : '') + '</span>';
                }).join('') +
                '</div></div>';
        }
        if (!html) html = '<div style="text-align:center;padding:20px;color:#e74c3c">未找到标签</div>';
        pbody.innerHTML = html;
        syncPreview();
        pbody.querySelectorAll('.dpe-delx').forEach(function(x) {
            x.addEventListener('click', function(e) {
                e.stopPropagation();
                var tg = x.closest('.dpe-tg');
                if (tg) { removeTag(tg.getAttribute('data-v')); render(data); }
            });
        });
        pbody.querySelectorAll('.dpe-tg').forEach(function(el) {
            el.addEventListener('click', function() {
                if (deleteMode) return;
                fbCopy(el.getAttribute('data-v'));
            });
        });
        if (lastKeyword) doSearch(lastKeyword);
    }

    // ==== 工具栏事件 ====
    delBtn.addEventListener('click', function() {
        deleteMode = !deleteMode;
        delBtn.classList.toggle('on', deleteMode);
        okBtn.style.display = deleteMode ? '' : 'none';
        toast(deleteMode ? '删除模式开启,点 × 删除' : '删除模式已关闭');
        render(data);
    });
    okBtn.addEventListener('click', function() {
        deleteMode = false;
        delBtn.classList.remove('on');
        okBtn.style.display = 'none';
        toast('已确认删除');
        render(data);
    });
    filterBtn.addEventListener('click', applyFilter);
    searchBtn.addEventListener('click', function() {
        var show = searchBox.style.display !== 'block';
        searchBox.style.display = show ? 'block' : 'none';
        if (show) { searchInput.focus(); } else { searchInput.value = ''; lastKeyword = ''; doSearch(''); }
    });
    searchInput.addEventListener('input', function() {
        lastKeyword = this.value.trim();
        doSearch(lastKeyword);
    });
    setBtn.addEventListener('click', function() {
        var show = setPanel.style.display !== 'block';
        setPanel.style.display = show ? 'block' : 'none';
        if (show) { renderIcons(); renderWords(); }
    });
    closeBtn.addEventListener('click', function() { closePanel(); });

    refreshBtn.addEventListener('click', function() { data = null; load(); });
    copyBtn.addEventListener('click', function() {
        var txt = preview.textContent;
        if (!txt) { toast('没有内容'); return; }
        if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(txt).then(function() {
                copyBtn.textContent = '✅ 已复制'; toast('已复制!');
                setTimeout(function() { copyBtn.textContent = '📋 复制'; }, 1800);
            }).catch(function() { fbCopy(txt); });
        } else { fbCopy(txt); }
    });

    applyMainIcon();

    console.log('[PromptExtractor] v1.1.0 loaded');
})();