Greasy Fork

Greasy Fork is available in English.

AI 提示词大师 Pro

稳定填充,支持分类折叠、半透明预览、原地编辑。针对 Gemini 启用 Trusted Types 策略,解决加载失败问题。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         AI 提示词大师 Pro
// @namespace    http://tampermonkey.net/
// @version      9.2.1
// @license      AGPL-3.0
// @description  稳定填充,支持分类折叠、半透明预览、原地编辑。针对 Gemini 启用 Trusted Types 策略,解决加载失败问题。
// @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://kimi.moonshot.cn/*
// @match        https://www.kimi.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ======= 0. 核心:TrustedHTML 安全策略 (解决 Gemini 报错) =======
    let ttPolicy;
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
        try {
            // 创建策略以允许合法的 HTML 字符串赋值
            ttPolicy = window.trustedTypes.createPolicy('pm-policy', {
                createHTML: (string) => string
            });
        } catch (e) {
            console.warn("PromptMaster: TrustedTypes policy already exists.");
        }
    }

    // 安全设置 innerHTML 的辅助函数
    const setHTML = (el, html) => {
        if (ttPolicy) {
            el.innerHTML = ttPolicy.createHTML(html);
        } else {
            el.innerHTML = html;
        }
    };

    // ======= 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: 2147483647; 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; 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; }

        /* 预览浮窗 (半透明、自适应) */
        #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;
        }

        /* 原地编辑器 */
        .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) {
                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("尝试填充失败");
        }
    }

    // ======= 4. UI 渲染引擎 (已接入 TrustedHTML 策略) =======
    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;
        })();
        setHTML(t, msg);
        t.style.display = 'block';
        setTimeout(() => t.style.display = 'none', 2000);
    }

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

        // 使用 setHTML 绕过安全拦截
        setHTML(root, `
            <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';
            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;
                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;

                btn.onmouseenter = (e) => {
                    if (isEditMode || previewLock) return;
                    const rect = btn.getBoundingClientRect();
                    const coreText = item.content.length > 70 ? item.content.substring(0, 70) + "..." : item.content;
                    setHTML(previewFloat, `<div style="font-weight:bold;margin-bottom:4px;color:#1a73e8;">预览</div>${coreText.replace(/\n/g, '<br>')}`);
                    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.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(); }
        };
    }

    // 编辑器相关逻辑
    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]; saveData(); }
            else renderUI();
        };
    }

    function deleteCat(catName) {
        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('');
        setHTML(editor, `
            <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></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-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;
            if(!isNew) promptData[cat].splice(idx, 1);
            promptData[tCat].push({ name: n, content: c });
            saveData(); renderUI();
        };
    }

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

    // ======= 6. 强力启动机制 =======
    const mount = () => {
        if (document.body && !document.getElementById('pm-root')) {
            renderUI();
        }
    };

    const observer = new MutationObserver(() => {
        if (!document.getElementById('pm-root')) { mount(); }
    });

    const startObserver = () => {
        if (document.body) {
            observer.observe(document.body, { childList: true, subtree: false });
            mount();
        } else {
            setTimeout(startObserver, 100);
        }
    };

    startObserver();
    setInterval(mount, 2000);

})();