您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
点击"搜"后分两步串行且加延时:1) 写前缀,2) 发送。每步确认成功再到下一步,避免过快导致失败或连发;按钮可拖动并记忆位置;长内容自动优化处理
// ==UserScript== // @name ChatGPT快捷深度搜索 // @namespace http://tampermonkey.net/ // @version 1.8 // @description 点击"搜"后分两步串行且加延时:1) 写前缀,2) 发送。每步确认成功再到下一步,避免过快导致失败或连发;按钮可拖动并记忆位置;长内容自动优化处理 // @author schweigen // @match https://chatgpt.com/ // @match https://chatgpt.com/?* // @match https://chatgpt.com/c/* // @match https://chatgpt.com/g/* // @match https://chatgpt.com/share/* // @license MIT // @grant GM_setValue // @grant GM_getValue // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ===== 配置(可微调延时和阈值) ===== const DEFAULT_POSITION = { top: '30%', right: '0px' }; const DEFAULT_POSITION_THINK = { top: '50%', right: '0px' }; const LONG_CONTENT_THRESHOLD = 5000; // 超过此字符数视为长内容,使用优化处理 const TIMEOUTS = { editorCommit: 2000, // 等待“写入生效”的最大时间 findSendBtn: 8000, // 等待找到发送按钮的最大时间 btnEnable: 1500, // 等待按钮可点 }; const DELAYS = { afterInsert: 160, // 写入后等一会 beforeClick: 80, // 点击前留一点时间 afterClickClear: 140, // 点击后再清空 unlockBtn: 2000, // 解锁按钮延时 nextClickWindow: 5000, // 防重复点击窗口 }; const POLL_INTERVAL = 70; // 轮询间隔 const PREFIX = `ultra think and deeper websearch `; const THINK_PREFIX = `think deeper `; const SEND_BTN_SELECTORS = [ 'button[data-testid="send-button"]', 'button#composer-submit-button[data-testid="send-button"]', 'form button[type="submit"][data-testid="send-button"]', 'form button[type="submit"]' ]; // ===== 状态 ===== let buttonPosition = GM_getValue('o4MiniButtonPosition', DEFAULT_POSITION); let thinkButtonPosition = GM_getValue('o4ThinkButtonPosition', DEFAULT_POSITION_THINK); let pendingModelSwitch = false; // 点“搜”后,仅下一次请求切模型 let isSending = false; // 防重入 let cycle = 0; // 事务编号 // ===== 拦截 fetch:仅切模型为 gpt-5 ===== const originalFetch = window.fetch; window.fetch = async function (url, options) { try { if ( pendingModelSwitch && typeof url === 'string' && url.endsWith('/backend-api/conversation') && options?.method === 'POST' && options?.body ) { let body; try { body = JSON.parse(options.body); } catch (_) { body = null; } if (body) { body.model = 'gpt-5'; options.body = JSON.stringify(body); } pendingModelSwitch = false; // 只改一次 } } catch (e) { console.warn('fetch hook error:', e); } return originalFetch(url, options); }; // ===== 小工具 ===== const sleep = (ms) => new Promise(r => setTimeout(r, ms)); function editorEl() { return document.querySelector('#prompt-textarea.ProseMirror, .ProseMirror'); } function editorFallback() { return document.querySelector('textarea[name="prompt-textarea"]'); } function editorText() { const el = editorEl(); if (el && typeof el.innerText === 'string') return (el.innerText || '').trim(); const fb = editorFallback(); return fb && typeof fb.value === 'string' ? fb.value.trim() : ''; } function isLongContent() { // 快速检测是否为长内容,避免完整获取文本 const el = editorEl(); if (el && typeof el.innerText === 'string') { return el.innerText.length > LONG_CONTENT_THRESHOLD; } const fb = editorFallback(); if (fb && typeof fb.value === 'string') { return fb.value.length > LONG_CONTENT_THRESHOLD; } return false; } function waitUntil(condFn, timeout = 1000, step = 50) { return new Promise((resolve, reject) => { const deadline = Date.now() + timeout; (function poll () { try { const v = condFn(); if (v) return resolve(v); if (Date.now() > deadline) return reject(new Error('timeout')); setTimeout(poll, step); } catch (e) { reject(e); } })(); }); } function lockButton(btn, lock) { if (!btn) return; btn.setAttribute('aria-disabled', lock ? 'true' : 'false'); btn.disabled = !!lock; } function clearEditorSafely() { const pm = editorEl(); if (pm) { pm.focus(); const r = document.createRange(); r.selectNodeContents(pm); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r); document.execCommand('insertText', false, ''); pm.dispatchEvent(new InputEvent('input', { bubbles: true })); pm.blur(); pm.focus(); return; } const fb = editorFallback(); if (fb) { fb.focus(); fb.value = ''; fb.dispatchEvent(new InputEvent('input', { bubbles: true })); fb.blur(); fb.focus(); } } // ===== 主流程:严格分步 + 确认 + 延时 ===== async function runPrefixThenSend(prefixText) { if (isSending) return; isSending = true; const myCycle = ++cycle; try { // 第 1 步:写前缀 insertPrefixAtBeginning(prefixText); await sleep(DELAYS.afterInsert); await waitUntil(() => editorText().startsWith(prefixText), TIMEOUTS.editorCommit, POLL_INTERVAL); // 第 2 步:等待发送按钮 → 锁 → 切模型 → 点击 const btn = await waitUntil(findSendButton, TIMEOUTS.findSendBtn, POLL_INTERVAL); await waitUntil(() => btn && !isDisabled(btn), TIMEOUTS.btnEnable, POLL_INTERVAL); lockButton(btn, true); pendingModelSwitch = true; await sleep(DELAYS.beforeClick); realClick(btn); // 清空编辑器,避免草稿回放再次发送 await sleep(DELAYS.afterClickClear); clearEditorSafely(); // 解锁 setTimeout(() => lockButton(btn, false), DELAYS.unlockBtn); } catch (e) { console.warn('pipeline error:', e); } finally { setTimeout(() => { if (cycle === myCycle) isSending = false; }, DELAYS.nextClickWindow); } } // 将 PREFIX 插入到输入框最前面(不重复) function insertPrefixAtBeginning(prefixText) { const pm = editorEl(); const fallback = editorFallback(); // 检查是否为长内容,使用不同的处理策略 const isLong = isLongContent(); if (isLong) { // 长内容优化处理:先清空再插入,避免大选区操作 const currentText = editorText(); const finalText = currentText.startsWith(prefixText) ? currentText : (prefixText + currentText); if (pm) { pm.focus(); // 先清空 document.execCommand('selectAll', false, null); document.execCommand('insertText', false, ''); // 再插入完整内容 document.execCommand('insertText', false, finalText); pm.dispatchEvent(new InputEvent('input', { bubbles: true })); pm.blur(); pm.focus(); return; } if (fallback) { fallback.focus(); fallback.value = finalText; fallback.dispatchEvent(new InputEvent('input', { bubbles: true })); fallback.blur(); fallback.focus(); } return; } // 短内容正常处理 const currentText = editorText(); const finalText = currentText ? (currentText.startsWith(prefixText) ? currentText : (prefixText + currentText)) : prefixText; if (pm) { pm.focus(); const range = document.createRange(); range.selectNodeContents(pm); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); document.execCommand('insertText', false, finalText); pm.dispatchEvent(new InputEvent('input', { bubbles: true })); pm.blur(); pm.focus(); return; } if (fallback) { fallback.focus(); fallback.value = finalText; fallback.dispatchEvent(new InputEvent('input', { bubbles: true })); fallback.blur(); fallback.focus(); } } function findSendButton() { for (const sel of SEND_BTN_SELECTORS) { const btn = document.querySelector(sel); if (btn) return btn; } return null; } function isDisabled(btn) { const aria = btn.getAttribute('aria-disabled'); return btn.disabled || aria === 'true'; } function realClick(btn) { try { // 优先用表单提交 const form = btn.closest('form'); if (form) { if (typeof form.requestSubmit === 'function') { form.requestSubmit(btn); } else { form.submit(); } return true; } // 退化到合成事件 const rect = btn.getBoundingClientRect(); const cx = Math.max(0, rect.left + rect.width / 2); const cy = Math.max(0, rect.top + rect.height / 2); const events = [ new PointerEvent('pointerdown', { bubbles: true, clientX: cx, clientY: cy }), new MouseEvent('mousedown', { bubbles: true, clientX: cx, clientY: cy }), new PointerEvent('pointerup', { bubbles: true, clientX: cx, clientY: cy }), new MouseEvent('mouseup', { bubbles: true, clientX: cx, clientY: cy }), ]; for (const ev of events) btn.dispatchEvent(ev); btn.click(); return true; } catch (e) { console.warn('realClick error:', e); return false; } } // ===== UI:按钮拖动 ===== function makeDraggable(el, onSavePosition) { // 统一使用 Pointer 事件;按位移阈值识别拖动,释放时保存位置 let isDragging = false; let pointerId = null; let startClientY = 0; let startTopPx = 0; const DRAG_THRESHOLD_PX = 6; // 超过该位移才视为拖动 // 防止触摸滚动干扰拖动 try { el.style.touchAction = 'none'; } catch (_) {} function toPxTop(value) { // 将任意 top(可能是百分比或 px)转换为 px 数值 if (!value) return 0; if (String(value).endsWith('%')) { const percent = parseFloat(value) || 0; return window.innerHeight * (percent / 100); } const n = parseFloat(value); return Number.isFinite(n) ? n : 0; } function clampTop(px) { const maxTop = Math.max(0, window.innerHeight - el.offsetHeight); return Math.max(0, Math.min(px, maxTop)); } function onPointerDown(e) { // 仅主指针/左键触发 if (e.button !== undefined && e.button !== 0) return; pointerId = e.pointerId || 'mouse'; el.setPointerCapture && el.setPointerCapture(e.pointerId); const comp = getComputedStyle(el); startTopPx = toPxTop(comp.top); startClientY = e.clientY; isDragging = false; // 尚未到达阈值 } function onPointerMove(e) { if ((e.pointerId || 'mouse') !== pointerId) return; const deltaY = e.clientY - startClientY; if (!isDragging && Math.abs(deltaY) >= DRAG_THRESHOLD_PX) { isDragging = true; el.style.cursor = 'move'; } if (isDragging) { const nextTop = clampTop(startTopPx + deltaY); el.style.top = `${Math.round(nextTop)}px`; // 阻止页面选择/滚动 e.preventDefault(); e.stopPropagation(); } } function onPointerUp(e) { if ((e.pointerId || 'mouse') !== pointerId) return; try { el.releasePointerCapture && el.releasePointerCapture(e.pointerId); } catch (_) {} if (isDragging) { // 标记抑制下一次 click el._suppressNextClick = true; el.style.cursor = 'pointer'; // 交由回调保存位置与提示 if (typeof onSavePosition === 'function') { onSavePosition({ top: el.style.top, right: el.style.right }); } } isDragging = false; pointerId = null; } el.addEventListener('pointerdown', onPointerDown, { passive: true }); window.addEventListener('pointermove', onPointerMove, { passive: false }); window.addEventListener('pointerup', onPointerUp, { passive: true }); } // ===== 创建“搜”按钮 ===== function addQuickSearchButton() { if (document.getElementById('o4-mini-button')) return; const btn = document.createElement('div'); btn.id = 'o4-mini-button'; btn.style.cssText = ` position: fixed; top: ${buttonPosition.top}; right: ${buttonPosition.right}; z-index: 2147483647; display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; background: linear-gradient(140.91deg, #7367F0 12.61%, #574AB8 76.89%); color: #fff; border-top-left-radius: 6px; border-bottom-left-radius: 6px; font-weight: 700; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,.2); transition: background .3s ease; font-size: 18px; user-select: none; touch-action: none; `; btn.textContent = '搜'; makeDraggable(btn, ({ top, right }) => { buttonPosition = { top, right }; GM_setValue('o4MiniButtonPosition', buttonPosition); notify('“搜”按钮位置已保存'); }); btn.addEventListener('click', function (e) { if (this._suppressNextClick) { this._suppressNextClick = false; return; } runPrefixThenSend(PREFIX); this.style.background = 'linear-gradient(140.91deg, #2ecc71 12.61%, #3498db 76.89%)'; setTimeout(() => { this.style.background = 'linear-gradient(140.91deg, #7367F0 12.61%, #574AB8 76.89%)'; }, 2000); notify('"搜"已激活:1)写前缀→2)发送(逐步确认+延时)'); }); document.body.appendChild(btn); } // ===== 创建“思”按钮 ===== function addThinkButton() { if (document.getElementById('o4-think-button')) return; const btn = document.createElement('div'); btn.id = 'o4-think-button'; btn.style.cssText = ` position: fixed; top: ${thinkButtonPosition.top}; right: ${thinkButtonPosition.right}; z-index: 2147483647; display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; background: linear-gradient(140.91deg, #FF6B6B 12.61%, #FF8E53 76.89%); color: #fff; border-top-left-radius: 6px; border-bottom-left-radius: 6px; font-weight: 700; cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,.2); transition: background .3s ease; font-size: 18px; user-select: none; touch-action: none; `; btn.textContent = '思'; makeDraggable(btn, ({ top, right }) => { thinkButtonPosition = { top, right }; GM_setValue('o4ThinkButtonPosition', thinkButtonPosition); notify('“思”按钮位置已保存'); }); btn.addEventListener('click', function () { if (this._suppressNextClick) { this._suppressNextClick = false; return; } runPrefixThenSend(THINK_PREFIX); this.style.background = 'linear-gradient(140.91deg, #27ae60 12.61%, #2ecc71 76.89%)'; setTimeout(() => { this.style.background = 'linear-gradient(140.91deg, #FF6B6B 12.61%, #FF8E53 76.89%)'; }, 2000); notify('"思"已激活:1)写前缀→2)发送(逐步确认+延时)'); }); document.body.appendChild(btn); } // ===== 提示 ===== function notify(msg) { const n = document.createElement('div'); n.style.cssText = ` position: fixed; top: 20px; right: 20px; background: rgba(0,0,0,.8); color: #fff; padding: 10px 20px; border-radius: 4px; z-index: 2147483647; transition: opacity .3s ease; `; n.textContent = msg; document.body.appendChild(n); setTimeout(() => { n.style.opacity = '0'; setTimeout(() => n.remove(), 300); }, 2000); } // ===== 注入与保活 ===== function addInlineButtons() { // 适配新版 DOM:优先找旧容器,找不到则找新版 composer 的 trailing 区域 let container = document.querySelector('div[data-testid="composer-trailing-actions"]'); if (!container) { // 新版:form[data-type="unified-composer"] 下的 [grid-area:trailing] 区域 container = document.querySelector('form[data-type="unified-composer"] div[class*="[grid-area:trailing]"]'); // 退化方案:依据语音按钮容器向上取父级 if (!container) { const speechContainer = document.querySelector('div[data-testid="composer-speech-button-container"]'); if (speechContainer && speechContainer.parentElement) { container = speechContainer.parentElement; } } } if (!container) return false; if (document.getElementById('o4-mini-inline-btn') || document.getElementById('o4-think-inline-btn')) return true; const wrap = document.createElement('div'); wrap.style.cssText = 'display:flex; align-items:center; gap:6px;'; const commonBtnCss = ` display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:9999px; color:#fff; box-shadow: 0 2px 8px rgba(0,0,0,.18); cursor:pointer; user-select:none; transition:opacity .2s ease, background .3s ease; font-weight:700; font-size:14px; `; // “搜” const searchBtn = document.createElement('div'); searchBtn.id = 'o4-mini-inline-btn'; searchBtn.style.cssText = commonBtnCss + 'background: linear-gradient(140.91deg, #7367F0 12.61%, #574AB8 76.89%);'; searchBtn.textContent = '搜'; searchBtn.addEventListener('click', function () { runPrefixThenSend(PREFIX); this.style.background = 'linear-gradient(140.91deg, #2ecc71 12.61%, #3498db 76.89%)'; setTimeout(() => { this.style.background = 'linear-gradient(140.91deg, #7367F0 12.61%, #574AB8 76.89%)'; }, 2000); notify('"搜"已激活:1)写前缀→2)发送(逐步确认+延时)'); }); // “思” const thinkBtn = document.createElement('div'); thinkBtn.id = 'o4-think-inline-btn'; thinkBtn.style.cssText = commonBtnCss + 'background: linear-gradient(140.91deg, #FF6B6B 12.61%, #FF8E53 76.89%);'; thinkBtn.textContent = '思'; thinkBtn.addEventListener('click', function () { runPrefixThenSend(THINK_PREFIX); this.style.background = 'linear-gradient(140.91deg, #27ae60 12.61%, #2ecc71 76.89%)'; setTimeout(() => { this.style.background = 'linear-gradient(140.91deg, #FF6B6B 12.61%, #FF8E53 76.89%)'; }, 2000); notify('"思"已激活:1)写前缀→2)发送(逐步确认+延时)'); }); wrap.appendChild(searchBtn); wrap.appendChild(thinkBtn); container.appendChild(wrap); return true; } function removeFloatingButtonsIfAny() { const a = document.getElementById('o4-mini-button'); const b = document.getElementById('o4-think-button'); if (a) a.remove(); if (b) b.remove(); } function boot() { if (!document.body) return; const inlineOk = addInlineButtons(); if (inlineOk) { // 如果已经成功放到输入区,就不再显示浮动按钮 removeFloatingButtonsIfAny(); return; } // 找不到输入区时,回退到浮动按钮 addQuickSearchButton(); addThinkButton(); } if (document.readyState === 'complete' || document.readyState === 'interactive') { boot(); } else { document.addEventListener('DOMContentLoaded', boot); } setInterval(boot, 2000); })();