Danbooru Prompt Extractor

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         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');
})();