// ==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);
})();