Greasy Fork

Greasy Fork is available in English.

AI 提示词大师 Pro

全能整合版:已内置专属配置。支持云端/本地切换、面板折叠、UI自适应、模板删除、智能ID修复。

当前为 2025-12-23 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         AI 提示词大师 Pro
// @namespace    http://tampermonkey.net/
// @version      10.0.4
// @license      MIT
// @description  全能整合版:已内置专属配置。支持云端/本地切换、面板折叠、UI自适应、模板删除、智能ID修复。
// @author       WaterHuo
// @match        *://gemini.google.com/*
// @match        *://chatgpt.com/*
// @match        *://claude.ai/*
// @match        *://chat.deepseek.com/*
// @match        *://www.doubao.com/*
// @match        *://www.kimi.ai/*
// @match        *://www.kimi.com/*
// @match        *://kimi.moonshot.cn/*
// @match        *://grok.com/*
// @match        *://x.com/i/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      tfntmhg1.api.lncldglobal.com
// @connect      v24uxbjt.lc-cn-n1-shared.com
// @connect      lc-cn-n1-shared.com
// @connect      api.lncldglobal.com
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ======= 0. 配置与工具函数 (已替换为你提供的专属配置) =======
    const CONFIG = {
        LC_ID: 'V24uxBjtrCUXygv7KjFtlJV0-gzGzoHsz',
        LC_KEY: 'n5nEW5FUV3YtKghWpGxwd4JK',
        API_URL: 'https://v24uxbjt.lc-cn-n1-shared.com/1.1/classes/PromptData'
    };

    // 获取/生成用户唯一标识
    const getUserId = () => {
        let uid = GM_getValue('pm_uid');
        if (!uid) {
            uid = 'user_' + Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
            GM_setValue('pm_uid', uid);
        }
        return uid;
    };
    const USER_ID = getUserId();

    // TrustedHTML 安全策略
    let ttPolicy;
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
        try {
            const policyName = 'pm-policy-' + Math.random().toString(36).substring(7);
            ttPolicy = window.trustedTypes.createPolicy(policyName, {
                createHTML: (string) => string,
                createScript: (string) => string
            });
        } catch (e) {
            console.warn("PromptMaster: TrustedTypes policy already exists");
        }
    }
    const setHTML = (el, html) => {
        if (!el) return;
        el.innerHTML = ttPolicy ? ttPolicy.createHTML(html) : html;
    };

    // ======= 1. 数据管理核心 =======
    const STORAGE_KEY = 'pm_data_v16';
    const MODE_KEY = 'pm_storage_mode';
    const APPEND_KEY = 'pm_append_mode';
    const CLOUD_OBJ_ID_KEY = 'pm_cloud_oid';
    const FOLD_KEY = 'pm_folded_cats';
    const MINIMIZED_KEY = 'pm_is_minimized';

    let currentMode = GM_getValue(MODE_KEY, 'local');
    let promptData = {};
    let foldedCats = GM_getValue(FOLD_KEY, []);
    let isEditMode = false;
    let appendMode = GM_getValue(APPEND_KEY, true);
    let isMinimized = GM_getValue(MINIMIZED_KEY, false);
    let isLoading = false;

    // 默认初始数据
    const defaultData = {
        "写作类": [{ name: "📝 深度润色", content: "请优化以下文本,梳理逻辑脉络,提升语言表达的简洁性和流畅性,保留核心信息。" }],
        "代码类": [{ name: "💻 逻辑审查", content: "请检查这段代码的语法错误、逻辑漏洞,给出优化建议和修复方案。" }]
    };

    // 云端API封装
    const CloudAPI = {
        headers: {
            "X-LC-Id": CONFIG.LC_ID,
            "X-LC-Key": CONFIG.LC_KEY,
            "Content-Type": "application/json"
        },
        request(method, endpoint, data = null) {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: method,
                    url: endpoint.startsWith('http') ? endpoint : CONFIG.API_URL + endpoint,
                    headers: this.headers,
                    data: data ? JSON.stringify(data) : null,
                    onload: (res) => {
                        if (res.status >= 200 && res.status < 300) {
                            resolve(JSON.parse(res.responseText));
                        } else {
                            try {
                                reject(JSON.parse(res.responseText));
                            } catch (e) {
                                reject({ error: res.statusText, code: res.status });
                            }
                        }
                    },
                    onerror: (err) => reject({ error: 'Network Error', code: 0 })
                });
            });
        },
        async fetchData() {
            try {
                const res = await this.request('GET', `?where={"uid":"${USER_ID}"}`);
                if (res.results && res.results.length > 0) {
                    GM_setValue(CLOUD_OBJ_ID_KEY, res.results[0].objectId);
                    let remoteData = res.results[0].data;
                    // 兼容旧格式字符串解析
                    if (typeof remoteData === 'string') {
                        try { remoteData = JSON.parse(remoteData); } catch(e) {}
                    }
                    return remoteData;
                }
            } catch (e) {
                console.warn("Fetch failed:", e);
            }
            return null;
        },
        // 智能保存逻辑:自动处理对象不存在的情况
        async saveData(data) {
            let oid = GM_getValue(CLOUD_OBJ_ID_KEY);
            const payload = { uid: USER_ID, data: data };

            const createNew = async () => {
                const res = await this.request('POST', '', payload);
                GM_setValue(CLOUD_OBJ_ID_KEY, res.objectId);
                console.log("已自动创建新云端对象:", res.objectId);
            };

            if (oid) {
                try {
                    await this.request('PUT', `/${oid}`, payload);
                } catch (e) {
                    // 错误码 1 或 101 代表 Object not found (可能你在后台删了数据)
                    if (e.code === 1 || e.code === 101) {
                        console.warn("云端对象不存在,正在重新创建...");
                        await createNew();
                    } else {
                        throw e;
                    }
                }
            } else {
                await createNew();
            }
        }
    };

    // 统一数据管理器
    const DataManager = {
        async load() {
            isLoading = true;
            renderUI();
            try {
                if (currentMode === 'local') {
                    promptData = GM_getValue(STORAGE_KEY) || defaultData;
                } else {
                    const cloudData = await CloudAPI.fetchData();
                    promptData = cloudData || defaultData;
                }
            } catch (e) {
                toast(`加载失败: ${e.error || '网络错误'}`);
                promptData = defaultData;
            } finally {
                isLoading = false;
                renderUI();
            }
        },
        async save() {
            renderUI();
            try {
                if (currentMode === 'local') {
                    GM_setValue(STORAGE_KEY, promptData);
                    toast("✅ 本地已保存");
                } else {
                    toast("☁️ 正在同步云端...", 5000);
                    await CloudAPI.saveData(promptData);
                    toast("✅ 云端同步完成");
                }
            } catch (e) {
                toast("❌ 保存失败,请检查网络");
                console.error(e);
            }
        }
    };

    // ======= 2. 导入导出工具 =======
    const IOTools = {
        exportJSON() {
            const fileName = `prompt_master_${currentMode}_${new Date().toISOString().slice(0,10)}.json`;
            const blob = new Blob([JSON.stringify(promptData, null, 2)], { type: "application/json" });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = fileName;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            toast("✅ 已导出备份");
        },
        importJSON() {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json';
            input.style.display = 'none';
            document.body.appendChild(input);
            input.onchange = (e) => {
                const file = e.target.files[0];
                if (!file) return;
                const reader = new FileReader();
                reader.onload = (event) => {
                    try {
                        const newData = JSON.parse(event.target.result);
                        if (typeof newData === 'object' && newData !== null) {
                            let addedCount = 0;
                            Object.keys(newData).forEach(cat => {
                                if (!promptData[cat]) {
                                    promptData[cat] = newData[cat];
                                    addedCount += newData[cat].length;
                                } else {
                                    const existingNames = new Set(promptData[cat].map(item => item.name));
                                    newData[cat].forEach(newItem => {
                                        if (!existingNames.has(newItem.name)) {
                                            promptData[cat].push(newItem);
                                            addedCount++;
                                        }
                                    });
                                }
                            });
                            if (addedCount > 0) {
                                DataManager.save();
                                alert(`✅ 导入成功!新增 ${addedCount} 条。`);
                            } else {
                                alert("⚠️ 导入完成,没有新增内容。");
                            }
                        } else { toast("❌ 格式错误"); }
                    } catch (err) { toast("❌ 解析失败"); }
                    document.body.removeChild(input);
                };
                reader.readAsText(file);
            };
            input.click();
        }
    };

    // ======= 3. 样式表 =======
    GM_addStyle(`
        #pm-root { font-family: -apple-system, system-ui, sans-serif; }
        .pm-panel {
            position: fixed; top: 80px; right: 20px;
            width: auto; min-width: 260px; max-width: 350px;
            background: #fff; border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.1);
            z-index: 2147483647; border: 1px solid #eee;
            display: flex; flex-direction: column;
            transition: width 0.2s ease;
        }
        .pm-float-ball {
            position: fixed; top: 80px; right: 20px;
            width: 40px; height: 40px; background: #1a73e8;
            border-radius: 50%; box-shadow: 0 4px 12px rgba(26,115,232,0.3);
            z-index: 2147483647; cursor: pointer;
            display: flex; justify-content: center; align-items: center;
            color: #fff; font-weight: bold; font-size: 14px;
            border: 2px solid #fff; transition: transform 0.2s; user-select: none;
        }
        .pm-float-ball:hover { transform: scale(1.1); background: #1557b0; }
        .pm-header {
            padding: 10px 12px; background: #fcfcfc; border-bottom: 1px solid #f0f0f0;
            display: flex; justify-content: space-between; align-items: center;
            border-radius: 12px 12px 0 0; flex-wrap: wrap; gap: 8px;
        }
        .pm-title-area { display: flex; align-items: center; gap: 6px; flex: 1 1 auto; }
        .pm-title { font-size: 14px; font-weight: 700; color: #1a73e8; white-space: nowrap; }
        .pm-badge {
            font-size: 11px; padding: 3px 6px; border-radius: 4px; cursor: pointer;
            display: flex; align-items: center; border: 1px solid transparent; user-select: none; white-space: nowrap;
        }
        .pm-badge:hover { filter: brightness(0.95); transform: translateY(-1px); }
        .mode-local { background: #e6f4ea; color: #137333; border-color: #ceead6; }
        .mode-cloud { background: #e8f0fe; color: #1967d2; border-color: #d2e3fc; }
        .fill-append { background: #f5f9ff; color: #406599; border-color: #e1eafc; }
        .fill-replace { background: #f8f0f5; color: #8b5cf6; border-color: #f3e8ff; }
        .pm-header-right { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }
        .pm-icon-btn {
            padding: 5px; border-radius: 4px; cursor: pointer; font-size: 14px;
            display: flex; align-items: center; justify-content: center; color: #5f6368;
        }
        .pm-icon-btn:hover { background: #f1f3f4; color: #1a73e8; }
        .pm-icon-active { color: #1a73e8; font-weight:bold; background: #e8f0fe; }
        .pm-body { padding: 8px; max-height: 60vh; overflow-y: auto; scrollbar-width: thin; min-height: 100px; position: relative;}
        .pm-footer {
            padding: 8px; border-top: 1px solid #eee; display: flex; gap: 6px;
            background: #fff; border-radius: 0 0 12px 12px;
        }
        .pm-tool-btn {
            flex: 1; padding: 6px; border: 1px solid #f1f3f4; background: #f8f9fa;
            border-radius: 6px; font-size: 11px; cursor: pointer; color: #5f6368; white-space: nowrap;
        }
        .pm-tool-btn:hover { background: #e8f0fe; color: #1967d2; border-color: #d2e3fc; }
        .pm-cat-wrap { margin-bottom: 8px; border-radius: 8px; overflow: hidden; }
        .pm-cat-header {
            display: flex; align-items: center; padding: 6px 4px; background: #f8f9fa; cursor: pointer;
        }
        .pm-cat-name { font-size: 12px; color: #5f6368; font-weight: 700; flex: 1; text-transform: uppercase; }
        .pm-cat-tools { display: none; gap: 6px; margin-right: 4px; }
        .pm-cat-header:hover .pm-cat-tools { display: flex; }
        .pm-tpl-list { padding: 4px 0; }
        .pm-tpl-list.folded { display: none; }
        .pm-item-wrap { position: relative; margin-bottom: 2px; }
        .pm-btn {
            width: 100%; border: none; background: transparent; padding: 8px 10px;
            text-align: left; font-size: 13px; border-radius: 6px; cursor: pointer;
            color: #3c4043; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        }
        .pm-btn:hover { background: #f1f3f4; color: #1a73e8; }
        #pm-preview-float {
            position: fixed; display: none; width: auto; max-width: 280px;
            background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(8px);
            border: 1px solid rgba(0,0,0,0.08); box-shadow: 0 4px 16px rgba(0,0,0,0.1);
            border-radius: 8px; padding: 10px; font-size: 12px; line-height: 1.5;
            color: #444; z-index: 2147483647; opacity: 0; transition: opacity 0.2s;
        }
        #pm-preview-float.show { opacity: 1; }
        .pm-inline-editor {
            background: #fff; border: 1px solid #1a73e8; border-radius: 8px;
            padding: 8px; margin: 4px 0; box-shadow: 0 4px 12px rgba(26,115,232,0.15);
        }
        .pm-inline-editor textarea {
            width: 100%; height: 100px; border: 1px solid #dadce0;
            border-radius: 4px; padding: 6px; font-size: 12px; box-sizing: border-box; margin: 5px 0;
        }
        .pm-inline-editor input, .pm-inline-editor select {
            width: 100%; border: 1px solid #dadce0; border-radius: 4px;
            padding: 4px 6px; font-size: 12px; box-sizing: border-box; margin-bottom: 4px;
        }
        .pm-ed-btns { display: flex; justify-content: flex-end; gap: 6px; align-items: center; }
        .pm-ebtn { padding: 3px 8px; font-size: 11px; border-radius: 4px; cursor: pointer; border: none; }
        .pm-save { background: #1a73e8; color: #fff; }
        .pm-del { background: #fce8e6; color: #d93025; margin-right: auto; }
        .pm-del:hover { background: #f6c5c0; }
        .pm-cancel { background: #f1f3f4; color: #5f6368; }
        .pm-loading {
            position: absolute; top:0; left:0; width:100%; height:100%;
            background:rgba(255,255,255,0.8); display:flex; justify-content:center;
            align-items:center; font-size:12px; color:#666; z-index:10;
        }
        .pm-toast {
            position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
            background: #323232; color: #fff; padding: 6px 16px; border-radius: 16px;
            font-size: 12px; z-index: 2147483647; display: none;
        }
    `);

    // ======= 4. 稳定填充核心 =======
    async function stableInject(text) {
        const inputField = document.querySelector(
            'div[role="textbox"], #prompt-textarea, textarea[placeholder*="输入"], #chat-input, [contenteditable="true"], textarea'
        );
        if (!inputField) return toast("❌ 未找到输入框");

        inputField.focus();
        const isRich = inputField.isContentEditable;
        const oldVal = isRich ? inputField.innerText : inputField.value;
        const newVal = (appendMode && oldVal.trim()) ? (oldVal + "\n" + text) : text;

        try {
            if (isRich) {
                const selection = window.getSelection();
                const range = document.createRange();
                range.selectNodeContents(inputField);
                selection.removeAllRanges();
                selection.addRange(range);
                document.execCommand('delete', false);
                document.execCommand('insertText', false, newVal);
            } else {
                const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
                setter.call(inputField, newVal);
                inputField.dispatchEvent(new Event('input', { bubbles: true }));
            }
            toast("✅ 填充成功");
            inputField.focus();
            setTimeout(() => { inputField.scrollTop = inputField.scrollHeight; }, 10);
        } catch (e) { toast("❌ 填充失败"); }
    }

    // ======= 5. UI 渲染引擎 =======
    function toast(msg, time=2000) {
        const t = document.getElementById('pm-toast') || (() => {
            const d = document.createElement('div');
            d.id = 'pm-toast'; d.className = 'pm-toast';
            document.body.appendChild(d); return d;
        })();
        setHTML(t, msg);
        t.style.display = 'block'; setTimeout(() => t.style.display = 'none', time);
    }

    function renderUI() {
        if (!document.body) return;
        let root = document.getElementById('pm-root');
        if (!root) {
            root = document.createElement('div'); root.id = 'pm-root'; document.body.appendChild(root);
            const pf = document.createElement('div'); pf.id = 'pm-preview-float'; document.body.appendChild(pf);
        }
        const previewFloat = document.getElementById('pm-preview-float');

        if (isMinimized) {
            setHTML(root, `<div class="pm-float-ball" id="pm-restore-btn" title="点击展开">AI</div>`);
            document.getElementById('pm-restore-btn').onclick = () => {
                isMinimized = false; GM_setValue(MINIMIZED_KEY, false); renderUI();
            };
            return;
        }

        const modeClass = currentMode === 'local' ? 'mode-local' : 'mode-cloud';
        const modeText = currentMode === 'local' ? '🏠 本地' : '☁️ 云端';
        const fillClass = appendMode ? 'fill-append' : 'fill-replace';
        const fillText = appendMode ? '➕ 追加' : '🔄 替换';
        const configIcon = isEditMode ? '✅' : '⚙️';

        setHTML(root, `
            <div class="pm-panel">
                <div class="pm-header">
                    <div class="pm-title-area">
                        <span class="pm-title">提示词大师</span>
                        <span id="pm-switch-storage" class="pm-badge ${modeClass}" title="切换存储模式">${modeText}</span>
                    </div>
                    <div class="pm-header-right">
                        <span id="pm-switch-fill" class="pm-badge ${fillClass}" title="切换填充模式">${fillText}</span>
                        <span id="pm-config-btn" class="pm-icon-btn ${isEditMode?'pm-icon-active':''}" title="编辑管理">${configIcon}</span>
                        <span id="pm-fold-btn" class="pm-icon-btn" title="折叠面板" style="font-weight:bold; margin-left:2px;">−</span>
                    </div>
                </div>
                <div class="pm-body" id="pm-list-container">
                    ${isLoading ? '<div class="pm-loading">同步中...</div>' : ''}
                </div>
                <div class="pm-footer">
                    <button class="pm-tool-btn" id="pm-export-btn">📤 导出</button>
                    <button class="pm-tool-btn" id="pm-import-btn">📥 导入</button>
                    ${isEditMode ? `<button class="pm-tool-btn" id="pm-new-cat" style="background:#e8f0fe; color:#1a73e8;">+ 分类</button>` : ''}
                </div>
            </div>
        `);

        document.getElementById('pm-fold-btn').onclick = () => { isMinimized = true; GM_setValue(MINIMIZED_KEY, true); renderUI(); };
        if (isLoading) return;

        const container = document.getElementById('pm-list-container');
        Object.keys(promptData).forEach(cat => {
            const isFolded = foldedCats.includes(cat);
            const catWrap = document.createElement('div');
            catWrap.className = 'pm-cat-wrap';

            const header = document.createElement('div');
            header.className = 'pm-cat-header';
            setHTML(header, `
                <span class="pm-cat-fold-icon" style="transform: ${isFolded ? 'rotate(-90deg)' : 'rotate(0deg)'}">▼</span>
                <span class="pm-cat-name">${cat}</span>
                ${isEditMode ? `<div class="pm-cat-tools"><span class="pm-ed-cat" data-cat="${cat}">✏️</span><span class="pm-del-cat" data-cat="${cat}">×</span></div>` : ''}
            `);
            header.onclick = (e) => {
                if (e.target.closest('.pm-cat-tools')) return;
                foldedCats = isFolded ? foldedCats.filter(c => c !== cat) : [...foldedCats, cat];
                GM_setValue(FOLD_KEY, foldedCats); renderUI();
            };
            if (isEditMode) {
                header.querySelector('.pm-ed-cat').onclick = () => editCatName(catWrap, cat);
                header.querySelector('.pm-del-cat').onclick = () => deleteCat(cat);
            }
            catWrap.appendChild(header);

            const tplList = document.createElement('div');
            tplList.className = `pm-tpl-list ${isFolded ? 'folded' : ''}`;

            promptData[cat].forEach((item, idx) => {
                const itemWrap = document.createElement('div');
                itemWrap.className = 'pm-item-wrap';
                const btn = document.createElement('button');
                btn.className = 'pm-btn';
                btn.innerText = item.name;

                btn.onmouseenter = (e) => {
                    if (isEditMode) return;
                    const rect = btn.getBoundingClientRect();
                    setHTML(previewFloat, `<div style="font-weight: 600; color: #1a73e8; margin-bottom: 6px; border-bottom: 1px solid rgba(0,0,0,0.05); padding-bottom: 4px;">${item.name}</div>${(item.content.length > 150 ? item.content.substring(0, 150) + "..." : item.content).replace(/\n/g, '<br>')}`);
                    previewFloat.style.display = 'block'; previewFloat.classList.add('show');
                    let topPos = rect.top + window.scrollY, leftPos = rect.right + 15;
                    if (leftPos + previewFloat.offsetWidth > window.innerWidth) leftPos = rect.left - previewFloat.offsetWidth - 15;
                    if (topPos + previewFloat.offsetHeight > window.innerHeight + window.scrollY) topPos = rect.bottom - previewFloat.offsetHeight + window.scrollY;
                    previewFloat.style.top = `${topPos}px`; previewFloat.style.left = `${leftPos}px`;
                };
                btn.onmouseleave = () => { previewFloat.classList.remove('show'); setTimeout(() => { previewFloat.style.display = 'none'; }, 150); };

                btn.onclick = () => {
                    if (isEditMode) editTpl(itemWrap, cat, idx);
                    else { stableInject(item.content); previewFloat.style.display = 'none'; }
                };
                itemWrap.appendChild(btn); tplList.appendChild(itemWrap);
            });

            if (isEditMode) {
                const addTplBtn = document.createElement('button');
                addTplBtn.className = 'pm-btn'; addTplBtn.style.border = "1px dashed #ccc"; addTplBtn.innerText = "+ 新模板";
                addTplBtn.onclick = () => editTpl(tplList, cat, -1, addTplBtn);
                tplList.appendChild(addTplBtn);
            }
            catWrap.appendChild(tplList); container.appendChild(catWrap);
        });

        document.getElementById('pm-config-btn').onclick = () => { isEditMode = !isEditMode; renderUI(); };
        document.getElementById('pm-switch-fill').onclick = () => { appendMode = !appendMode; GM_setValue('pm_append_mode', appendMode); renderUI(); };
        document.getElementById('pm-switch-storage').onclick = async () => {
            if (isLoading) return;
            const targetMode = currentMode === 'local' ? '☁️ 云端' : '🏠 本地';
            if (confirm(`确认切换至 [${targetMode}] 模式?`)) {
                currentMode = currentMode === 'local' ? 'cloud' : 'local';
                GM_setValue(MODE_KEY, currentMode);
                await DataManager.load();
            }
        };
        document.getElementById('pm-export-btn').onclick = IOTools.exportJSON;
        document.getElementById('pm-import-btn').onclick = IOTools.importJSON;
        if (isEditMode) {
            document.getElementById('pm-new-cat').onclick = () => {
                const n = prompt("请输入新分类名称:");
                if (n) { promptData[n] = []; DataManager.save(); }
            };
        }
    }

    // ======= 6. 辅助编辑逻辑 =======
    function editCatName(wrap, oldName) {
        const header = wrap.querySelector('.pm-cat-header'); header.style.display = 'none';
        const editor = document.createElement('div'); editor.className = 'pm-inline-editor';
        setHTML(editor, `<input type="text" id="new-cat-inp" value="${oldName}"><div class="pm-ed-btns"><button class="pm-ebtn pm-cancel">取消</button><button class="pm-ebtn pm-save">保存</button></div>`);
        wrap.prepend(editor);
        editor.querySelector('.pm-cancel').onclick = () => renderUI();
        editor.querySelector('.pm-save').onclick = () => {
            const n = editor.querySelector('#new-cat-inp').value.trim();
            if (n && n !== oldName) { promptData[n] = promptData[oldName]; delete promptData[oldName]; DataManager.save(); } else renderUI();
        };
    }

    function deleteCat(catName) {
        if (!confirm(`确定删除分类 [${catName}] 吗?`)) return;
        delete promptData[catName]; DataManager.save();
    }

    function editTpl(container, cat, idx, addBtn = null) {
        const isNew = idx === -1;
        const item = isNew ? { name: "", content: "" } : promptData[cat][idx];
        const editor = document.createElement('div'); editor.className = 'pm-inline-editor';
        let cats = Object.keys(promptData).map(c => `<option value="${c}" ${c === cat ? 'selected' : ''}>${c}</option>`).join('');

        setHTML(editor, `
            <input type="text" id="ed-name" placeholder="模板名称" value="${item.name}">
            <select id="ed-cat">${cats}</select>
            <textarea id="ed-cont" placeholder="提示词内容...">${item.content}</textarea>
            <div class="pm-ed-btns">
                ${!isNew ? '<button class="pm-ebtn pm-del">🗑️ 删除</button>' : ''}
                <button class="pm-ebtn pm-cancel">取消</button>
                <button class="pm-ebtn pm-save">保存</button>
            </div>
        `);

        if (!isNew) container.querySelector('.pm-btn').style.display = 'none';
        if (addBtn) addBtn.style.display = 'none';
        container.appendChild(editor);

        editor.querySelector('.pm-cancel').onclick = () => renderUI();
        if (!isNew) {
            editor.querySelector('.pm-del').onclick = () => {
                if (confirm('确定要删除这个模板吗?')) {
                    promptData[cat].splice(idx, 1);
                    DataManager.save();
                }
            };
        }
        editor.querySelector('.pm-save').onclick = () => {
            const n = editor.querySelector('#ed-name').value.trim();
            const c = editor.querySelector('#ed-cont').value;
            const tCat = editor.querySelector('#ed-cat').value;
            if (!n || !c) return toast("❌ 名称和内容不能为空");
            if (!isNew) promptData[cat].splice(idx, 1);
            promptData[tCat].push({ name: n, content: c });
            DataManager.save();
        };
    }

    // ======= 7. 启动初始化 =======
    const init = async () => {
        if (!document.getElementById('pm-root')) { renderUI(); await DataManager.load(); }
    };
    const observer = new MutationObserver(() => { if (!document.getElementById('pm-root')) renderUI(); });
    const startObserver = () => {
        if (document.body) { observer.observe(document.body, { childList: true, subtree: false }); init(); }
        else setTimeout(startObserver, 100);
    };
    startObserver();
    setInterval(() => { if (document.body && !document.getElementById('pm-root')) renderUI(); }, 2000);
})();