Greasy Fork is available in English.
稳定填充,支持分类折叠、半透明预览、原地编辑。针对 Gemini 启用 Trusted Types 策略,解决加载失败问题。
当前为
// ==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);
})();