Greasy Fork

Greasy Fork is available in English.

AI 提示词大师 Pro 9.0 (稳定填充+分类折叠+预览优化)

解决填充消失,支持分类折叠、半透明自适应预览、原地编辑、追加模式。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         AI 提示词大师 Pro 9.0 (稳定填充+分类折叠+预览优化)
// @namespace    http://tampermonkey.net/
// @version      9.0
// @license      AGPL-3.0
// @description  解决填充消失,支持分类折叠、半透明自适应预览、原地编辑、追加模式。
// @author       WaterHuo
// @match        https://gemini.google.com/*
// @match        https://chatgpt.com/*
// @match        https://claude.ai/*
// @match        https://chat.deepseek.com/*
// @match        https://www.doubao.com/*
// @match        https://www.kimi.ai/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // ======= 1. 深度样式表 =======
    GM_addStyle(`
        #pm-root { font-family: -apple-system, system-ui, sans-serif; }
        .pm-panel { position: fixed; top: 80px; right: 20px; width: 220px; background: #fff; border-radius: 12px;
                    box-shadow: 0 8px 32px rgba(0,0,0,0.1); z-index: 2147483640; border: 1px solid #eee; display: flex; flex-direction: column; }
        .pm-header { padding: 12px; background: #fcfcfc; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; border-radius: 12px 12px 0 0; }
        .pm-title { font-size: 13px; font-weight: 700; color: #1a73e8; display: flex; align-items: center; gap: 5px; }
        .pm-body { padding: 8px; max-height: 75vh; overflow-y: auto; scrollbar-width: thin; }

        /* 分类与折叠 */
        .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; group; position: relative; }
        .pm-cat-fold-icon { font-size: 10px; margin-right: 6px; transition: transform 0.2s; color: #70757a; }
        .pm-cat-name { font-size: 11px; color: #5f6368; font-weight: 700; flex: 1; text-transform: uppercase; letter-spacing: 0.5px; }
        .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: 12px;
                  border-radius: 6px; cursor: pointer; color: #3c4043; transition: 0.2s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .pm-btn:hover { background: #f1f3f4; color: #1a73e8; }

        /* 预览浮窗 (需求2: 半透明、自适应) */
        #pm-preview-float {
            position: fixed; display: none; width: auto; max-width: 200px; 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: 11px; line-height: 1.4; color: #444; z-index: 2147483647;
            pointer-events: auto; word-break: break-all; transition: opacity 0.2s;
        }

        /* 原地编辑器 (需求3) */
        .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; resize: vertical; 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; }
        .pm-ebtn { padding: 3px 8px; font-size: 11px; border-radius: 4px; cursor: pointer; border: none; }
        .pm-save { background: #1a73e8; color: #fff; }
        .pm-cancel { background: #f1f3f4; color: #5f6368; }

        .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; }
    `);

    // ======= 2. 数据与持久化 =======
    const DATA_KEY = 'pm_data_v9';
    const FOLD_KEY = 'pm_folded_cats';
    let promptData = GM_getValue(DATA_KEY, {
        "写作类": [{ name: "📝 深度润色", content: "请对以下文本进行优化:先梳理逻辑脉络(若原文条理混乱,可按 “总分 / 因果 / 递进” 等清晰结构重组),再优化语言表达 —— 做到简洁精准、流畅自然,剔除冗余重复内容,完整保留原文核心信息与核心意图,不添加额外修饰,仅提升文本的逻辑性与可读性。" }],
        "代码类": [{ name: "💻 逻辑审查", content: "请检查这段代码是否存在潜在的逻辑错误..." }]
    });
    let foldedCats = GM_getValue(FOLD_KEY, []); // 存储折叠状态的分类名
    let isEditMode = false;
    let previewLock = false;
    let appendMode = true; // 默认追加模式

    // ======= 3. 稳定填充核心 (解决填充消失问题) =======
    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) {
                // 针对 Gemini/Claude/DeepSeek 富文本模式
                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 {
                // 针对 ChatGPT/豆包/Kimi 原生模式
                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("尝试填充失败");
        }
    }

    // ======= 4. UI 渲染引擎 =======
    function toast(msg) {
        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;
        })();
        t.innerText = msg; t.style.display = 'block';
        setTimeout(() => t.style.display = 'none', 2000);
    }

    function renderUI() {
        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');

        root.innerHTML = `
            <div class="pm-panel">
                <div class="pm-header">
                    <span class="pm-title">🪄 提示词大师 <small style="font-weight:normal; font-size:10px; margin-left:5px; color:#999;">${appendMode?'追加':'替换'}</small></span>
                    <div style="display:flex; gap:8px;">
                        <span id="pm-toggle-mode" style="cursor:pointer; font-size:12px;" title="切换 追加/替换 填充">${appendMode?'➕':'🔄'}</span>
                        <span id="pm-config-btn" style="cursor:pointer; font-size:13px;">${isEditMode ? '✅' : '⚙️'}</span>
                    </div>
                </div>
                <div class="pm-body" id="pm-list-container"></div>
                ${isEditMode ? `<div style="padding:8px; border-top:1px solid #eee"><button id="pm-new-cat" style="width:100%; padding:6px; border:none; background:#e8f0fe; color:#1a73e8; border-radius:6px; font-size:11px; cursor:pointer;">+ 新建分类</button></div>` : ''}
            </div>
        `;

        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';
            header.innerHTML = `
                <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;
                if (isFolded) foldedCats = foldedCats.filter(c => c !== cat);
                else foldedCats.push(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;

                // 预览触发 (需求2)
                btn.onmouseenter = (e) => {
                    if (isEditMode || previewLock) return;
                    const rect = btn.getBoundingClientRect();
                    const coreText = item.content.length > 70 ? item.content.substring(0, 70) + "..." : item.content;
                    previewFloat.innerHTML = `<div style="font-weight:bold;margin-bottom:4px;color:#1a73e8;">预览</div>${coreText.replace(/\n/g, '<br>')}<div style="font-size:9px;color:#aaa;margin-top:4px;">(点击填充,右侧支持选中复制)</div>`;
                    previewFloat.style.display = 'block';

                    // 自适应位置计算
                    let topPos = rect.top;
                    if (topPos + 150 > window.innerHeight) topPos = window.innerHeight - 160;
                    previewFloat.style.top = `${topPos}px`;
                    previewFloat.style.right = `${window.innerWidth - rect.left + 10}px`;
                };
                btn.onmouseleave = () => { if(!previewLock) previewFloat.style.display = 'none'; };

                btn.onclick = () => {
                    if (isEditMode) {
                        editTpl(itemWrap, cat, idx);
                    } else {
                        stableInject(item.content);
                        previewLock = true;
                        setTimeout(() => { previewLock = false; previewFloat.style.display='none'; }, 1500);
                    }
                };

                itemWrap.appendChild(btn);
                tplList.appendChild(itemWrap);
            });

            if (isEditMode) {
                const addTplBtn = document.createElement('button');
                addTplBtn.className = 'pm-btn'; addTplBtn.style.border = "1px dashed #ccc"; addTplBtn.style.color="#999";
                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-toggle-mode').onclick = () => { appendMode = !appendMode; renderUI(); };
        if (isEditMode) document.getElementById('pm-new-cat').onclick = () => {
            const n = prompt("输入新分类名称:"); if(n) { promptData[n]=[]; saveData(); }
        };
    }

    // ======= 5. 交互:原地编辑分类/模板 =======
    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';
        editor.innerHTML = `<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]; saveData(); }
            else renderUI();
        };
    }

    function deleteCat(catName) {
        if(promptData[catName].length > 0) {
            if(!confirm(`该分类下有模板,删除后将迁移至“通用”分类,确定吗?`)) return;
            if(!promptData["通用"]) promptData["通用"] = [];
            promptData["通用"] = promptData["通用"].concat(promptData[catName]);
        } else if(!confirm(`确定删除空分类 [${catName}]?`)) return;
        delete promptData[catName];
        saveData();
    }

    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('');

        editor.innerHTML = `
            <input type="text" id="ed-name" placeholder="名称" value="${item.name}">
            <select id="ed-cat">${cats}</select>
            <div style="position:relative"><textarea id="ed-cont" placeholder="提示词内容...">${item.content}</textarea><span id="pm-clear-ed" style="position:absolute;right:5px;top:5px;cursor:pointer;color:#ccc;">×</span></div>
            <div class="pm-ed-btns"><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-clear-ed').onclick = () => editor.querySelector('#ed-cont').value = "";
        editor.querySelector('.pm-cancel').onclick = () => renderUI();
        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 });
            saveData(); toast("保存成功");
        };
    }

    function saveData() { GM_setValue(DATA_KEY, promptData); renderUI(); }

    // ======= 6. 自动检测与启动 =======
    const init = () => { if (!document.getElementById('pm-root')) renderUI(); };
    setInterval(init, 2000); // 应对单页应用重绘
    setTimeout(init, 1000);
})();