// ==UserScript== // @name 豆包复制助手 // @namespace http://tampermonkey.net/ // @version 1.2 // @description 在豆包网页添加复制短语功能,适配新版ProseMirror编辑器 // @author anyphasy // @match https://www.doubao.com/chat/* // @icon https://lf-flow-web-cdn.doubao.com/obj/flow-doubao/doubao/chat/favicon.png // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // ==/UserScript== (function () { "use strict"; GM_addStyle(` .doubao-copy-container { position: fixed; top: 65px; right: 26px; z-index: 9999; background: white; border: 1px solid #ddd; border-radius: 8px; padding: 15px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 180px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .doubao-copy-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee; } .doubao-copy-title { font-size: 16px; font-weight: 600; color: #333; padding-right: 5px; } .doubao-manage-btn { background: #4a90e2; color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; } .doubao-manage-btn:hover { background: #3a7bc8; } .doubao-current-phrase { background: #f5f5f5; border-radius: 6px; padding: 12px; margin: 10px 0; word-wrap: break-word; line-height: 1.4; font-size: 14px; color: #333; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .doubao-controls { display: flex; gap: 8px; margin-top: 10px; align-items: center; } .doubao-copy-buttons { display: flex; flex-direction: column; gap: 8px; flex: 1; } .doubao-copy-buttons .doubao-btn { flex: 0; } .doubao-btn { flex: 1; padding: 8px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; } .doubao-copy-btn { background: #07c160; color: white; } .doubao-copy-btn:hover { background: #06ae56; } .doubao-prev-btn { background: #f0f0f0; color: #333; } .doubao-prev-btn:hover { background: #e0e0e0; } .doubao-next-btn { background: #f0f0f0; color: #333; } .doubao-next-btn:hover { background: #e0e0e0; } .doubao-index-display { text-align: center; font-size: 12px; color: #666; margin-top: 8px; } .doubao-manage-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; border: 1px solid #ddd; border-radius: 8px; padding: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 10000; min-width: 400px; max-width: 500px; } .doubao-manage-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 9999; } .doubao-phrase-list { max-height: 300px; overflow-y: auto; margin: 15px 0; border: 1px solid #eee; border-radius: 4px; padding: 10px; } .doubao-phrase-item { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #f0f0f0; } .doubao-phrase-text { flex: 1; margin-right: 10px; word-wrap: break-word; } .doubao-remove-btn { background: #ff4757; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer; font-size: 12px; } .doubao-add-phrase { display: flex; gap: 8px; margin-top: 15px; } .doubao-add-input { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px; } .doubao-add-btn { background: #07c160; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .doubao-manage-footer { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .doubao-close-btn { background: #f0f0f0; color: #333; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; } .doubao-save-btn { background: #4a90e2; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; } `); const DEFAULT_PHRASES = [ "提取文字", "这图老师又讲了什么,详细述说,每个部分、每个细节", ]; // ★ 改动1:选择器从 textarea 改为 ProseMirror contenteditable div const INPUT_BOX_SELECTOR = ".tiptap.ProseMirror"; console.log("加载豆包脚本 v2.0 (适配ProseMirror)"); function getPhrases() { const savedPhrases = GM_getValue("phrases"); return savedPhrases && savedPhrases.length > 0 ? savedPhrases : DEFAULT_PHRASES; } function getCurrentIndex() { return GM_getValue("currentIndex", 0); } // ★ 改动2:通用函数,向 ProseMirror 编辑器插入文本 function insertTextToEditor(text) { const editor = document.querySelector(INPUT_BOX_SELECTOR); if (!editor) { console.log("未找到输入框"); return false; } // 聚焦编辑器 editor.focus(); // 把光标移到末尾 const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); // false = 折叠到末尾 sel.removeAllRanges(); sel.addRange(range); // 用 execCommand 插入文本,ProseMirror 会自动捕获并更新内部状态 document.execCommand("insertText", false, text); return true; } function copyOnly(text) { navigator.clipboard .writeText(text) .then(() => showMessage("已复制到剪贴板!")) .catch((err) => { console.error("复制失败:", err); showMessage("复制失败,请手动复制"); }); } // ★ 改动3:追加到 contenteditable 编辑器 function copyAndAppend(text) { navigator.clipboard .writeText(text) .then(() => { showMessage("已复制并追加到输入框!"); const editor = document.querySelector(INPUT_BOX_SELECTOR); if (!editor) { console.log("未找到输入框"); return; } // 判断编辑器是否有内容(排除空白/placeholder) const hasContent = editor.textContent.trim().length > 0 && !editor.querySelector("p.is-empty"); editor.focus(); // 光标移到末尾 const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); // 如果已有内容,先换行 if (hasContent) { document.execCommand("insertText", false, "\n"); } document.execCommand("insertText", false, text); }) .catch((err) => { console.error("复制失败:", err); showMessage("复制失败,请手动复制"); }); } // ★ 改动4:追加并发送 function copyAppendAndSend(text) { navigator.clipboard .writeText(text) .then(() => { showMessage("已复制、追加并发送!"); const editor = document.querySelector(INPUT_BOX_SELECTOR); if (!editor) { console.log("未找到输入框"); return; } const hasContent = editor.textContent.trim().length > 0 && !editor.querySelector("p.is-empty"); editor.focus(); const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); if (hasContent) { document.execCommand("insertText", false, "\n"); } document.execCommand("insertText", false, text); // 模拟回车发送 setTimeout(() => { editor.focus(); const enterDown = new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Enter", code: "Enter", keyCode: 13, which: 13, }); editor.dispatchEvent(enterDown); const enterUp = new KeyboardEvent("keyup", { bubbles: true, cancelable: true, key: "Enter", code: "Enter", keyCode: 13, which: 13, }); editor.dispatchEvent(enterUp); }, 200); }) .catch((err) => { console.error("复制失败:", err); showMessage("复制失败,请手动复制"); }); } function showMessage(message) { const msg = document.createElement("div"); msg.textContent = message; msg.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: white; padding: 10px 20px; border-radius: 4px; z-index: 10001; font-size: 14px; `; document.body.appendChild(msg); setTimeout(() => msg.remove(), 1500); } function createMainUI() { const container = document.createElement("div"); container.className = "doubao-copy-container"; const phrases = getPhrases(); let currentIndex = getCurrentIndex(); if (currentIndex >= phrases.length) { currentIndex = 0; GM_setValue("currentIndex", 0); } const header = document.createElement("div"); header.className = "doubao-copy-header"; const title = document.createElement("div"); title.className = "doubao-copy-title"; title.textContent = "短语助手"; const manageBtn = document.createElement("button"); manageBtn.className = "doubao-manage-btn"; manageBtn.textContent = "管理短语"; manageBtn.onclick = showManagePanel; header.appendChild(title); header.appendChild(manageBtn); const phraseDisplay = document.createElement("div"); phraseDisplay.className = "doubao-current-phrase"; phraseDisplay.textContent = phrases[currentIndex]; const indexDisplay = document.createElement("div"); indexDisplay.className = "doubao-index-display"; indexDisplay.textContent = `${currentIndex + 1} / ${phrases.length}`; const controls = document.createElement("div"); controls.className = "doubao-controls"; const prevBtn = document.createElement("button"); prevBtn.className = "doubao-btn doubao-prev-btn"; prevBtn.innerHTML = "↑"; prevBtn.onclick = () => { currentIndex = (currentIndex - 1 + phrases.length) % phrases.length; GM_setValue("currentIndex", currentIndex); phraseDisplay.textContent = phrases[currentIndex]; indexDisplay.textContent = `${currentIndex + 1} / ${phrases.length}`; }; const copyButtonsContainer = document.createElement("div"); copyButtonsContainer.className = "doubao-copy-buttons"; const copyOnlyBtn = document.createElement("button"); copyOnlyBtn.className = "doubao-btn doubao-copy-btn"; copyOnlyBtn.innerHTML = "📋"; copyOnlyBtn.title = "只复制"; copyOnlyBtn.onclick = () => copyOnly(phrases[currentIndex]); const copyAppendBtn = document.createElement("button"); copyAppendBtn.className = "doubao-btn doubao-copy-btn"; copyAppendBtn.innerHTML = "📋+"; copyAppendBtn.title = "复制并追加"; copyAppendBtn.onclick = () => copyAndAppend(phrases[currentIndex]); const copySendBtn = document.createElement("button"); copySendBtn.className = "doubao-btn doubao-copy-btn"; copySendBtn.innerHTML = "📋↵"; copySendBtn.title = "复制、追加并发送"; copySendBtn.onclick = () => copyAppendAndSend(phrases[currentIndex]); copyButtonsContainer.appendChild(copyOnlyBtn); copyButtonsContainer.appendChild(copyAppendBtn); copyButtonsContainer.appendChild(copySendBtn); const nextBtn = document.createElement("button"); nextBtn.className = "doubao-btn doubao-next-btn"; nextBtn.innerHTML = "↓"; nextBtn.onclick = () => { currentIndex = (currentIndex + 1) % phrases.length; GM_setValue("currentIndex", currentIndex); phraseDisplay.textContent = phrases[currentIndex]; indexDisplay.textContent = `${currentIndex + 1} / ${phrases.length}`; }; controls.appendChild(prevBtn); controls.appendChild(copyButtonsContainer); controls.appendChild(nextBtn); container.appendChild(header); container.appendChild(phraseDisplay); container.appendChild(controls); container.appendChild(indexDisplay); return container; } function showManagePanel() { const overlay = document.createElement("div"); overlay.className = "doubao-manage-overlay"; overlay.onclick = () => { document.body.removeChild(overlay); document.body.removeChild(panel); }; const panel = document.createElement("div"); panel.className = "doubao-manage-panel"; const title = document.createElement("div"); title.style.cssText = "font-size: 18px; font-weight: 600; margin-bottom: 20px;"; title.textContent = "管理短语"; const phraseList = document.createElement("div"); phraseList.className = "doubao-phrase-list"; let phrases = getPhrases(); function renderPhraseList() { phraseList.innerHTML = ""; phrases.forEach((phrase, index) => { const item = document.createElement("div"); item.className = "doubao-phrase-item"; const textSpan = document.createElement("span"); textSpan.className = "doubao-phrase-text"; textSpan.textContent = phrase; const removeBtn = document.createElement("button"); removeBtn.className = "doubao-remove-btn"; removeBtn.textContent = "删除"; removeBtn.onclick = () => { phrases.splice(index, 1); renderPhraseList(); }; item.appendChild(textSpan); item.appendChild(removeBtn); phraseList.appendChild(item); }); } renderPhraseList(); const addContainer = document.createElement("div"); addContainer.className = "doubao-add-phrase"; const addInput = document.createElement("input"); addInput.className = "doubao-add-input"; addInput.type = "text"; addInput.placeholder = "输入新的短语..."; const addBtn = document.createElement("button"); addBtn.className = "doubao-add-btn"; addBtn.textContent = "添加"; addBtn.onclick = () => { const newPhrase = addInput.value.trim(); if (newPhrase) { phrases.push(newPhrase); addInput.value = ""; renderPhraseList(); } }; addContainer.appendChild(addInput); addContainer.appendChild(addBtn); const footer = document.createElement("div"); footer.className = "doubao-manage-footer"; const closeBtn = document.createElement("button"); closeBtn.className = "doubao-close-btn"; closeBtn.textContent = "取消"; closeBtn.onclick = () => { document.body.removeChild(overlay); document.body.removeChild(panel); }; const saveBtn = document.createElement("button"); saveBtn.className = "doubao-save-btn"; saveBtn.textContent = "保存"; saveBtn.onclick = () => { GM_setValue("phrases", phrases); let currentIndex = getCurrentIndex(); if (currentIndex >= phrases.length) { GM_setValue("currentIndex", 0); } document.body.removeChild(overlay); document.body.removeChild(panel); const oldContainer = document.querySelector(".doubao-copy-container"); if (oldContainer) oldContainer.remove(); document.body.appendChild(createMainUI()); }; footer.appendChild(closeBtn); footer.appendChild(saveBtn); panel.appendChild(title); panel.appendChild(phraseList); panel.appendChild(addContainer); panel.appendChild(footer); document.body.appendChild(overlay); document.body.appendChild(panel); } function init() { const oldContainer = document.querySelector(".doubao-copy-container"); if (oldContainer) oldContainer.remove(); document.body.appendChild(createMainUI()); } window.addEventListener("load", init); if (document.readyState === "complete") { init(); } })();