// ==UserScript== // @name ChatGPT模型选择器增强 // @namespace http://tampermonkey.net/ // @author schweigen // @version 1.1.7 // @description 增强 Main 模型选择器(黏性重排、防抖动、自定义项、丝滑切换、隐藏分组与Legacy);并集成“使用其他模型重试的模型选择器”快捷项与30秒强制模型窗口(自动触发原生项或重试);可以自定义模型顺序。特别鸣谢:attention1111(linux.do),gpt-5 // @match https://chatgpt.com/ // @match https://chatgpt.com/?model=* // @match https://chatgpt.com/?temporary-chat=* // @match https://chatgpt.com/c/* // @match https://chatgpt.com/g/* // @match https://chatgpt.com/share/* // @run-at document-idle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/547113/ChatGPT%E6%A8%A1%E5%9E%8B%E9%80%89%E6%8B%A9%E5%99%A8%E5%A2%9E%E5%BC%BA.user.js // @updateURL https://update.greasyfork.icu/scripts/547113/ChatGPT%E6%A8%A1%E5%9E%8B%E9%80%89%E6%8B%A9%E5%99%A8%E5%A2%9E%E5%BC%BA.meta.js // ==/UserScript== (() => { 'use strict'; const W = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; // schweigen said: 老虎机版vibe coding,我都不知道我在写什么,纯屎山,勿喷 :D // 特别鸣谢:attention1111(linux.do),gpt-5 // 现状说明: // 1) 临时聊天(temporary-chat)暂时无法增强“使用其他模型重试的模型选择器”。 // 2) Projects 中目前官方没有“重试”按钮,笔者太菜不知道怎么添加能抗住遥测的retry按钮。 // ========= 概览:本脚本处理“两个不同的菜单”并采用不同匹配/放置策略 ========= // 1) Main 模型选择器(Main Model Switcher) // - 匹配条件: // role="menu" | "listbox",且包含 Main 菜单签名 [data-testid^="model-switcher-"] 或 [data-cgpt-turn]; // 同时不包含“重试模型选择器”关键字(Auto/Instant/Thinking/Pro/Ultra)。 // - 放置策略: // 若无同名原生项,则在“最后一个 GPT‑5 原生项”之后插入自定义项,随后用 applyDesiredOrder 按订阅层级的目标顺序黏性重排。 // 并隐藏“Legacy models”相关入口/分隔线,尽量减少 hover 抖动和菜单关闭。 // - 点击行为: // 自定义项阻止冒泡后,先做“丝滑更新”(URL+按钮文案),再尝试点击同 id 的原生项,让后端状态切换。 // // 2) 使用其他模型重试的模型选择器(Auto/Instant/Thinking(mini)/Pro/Ultra(Think)) // - 匹配条件: // 菜单内出现上述关键字,且不含 Main 菜单签名(与 Main 模型选择器互斥)。 // - 放置策略: // 以锚点项为参照(优先 o4 mini / gpt‑4o / gpt‑4.1,找不到则取第一项),在其后插入 4 个快捷项: // o3 pro、GPT 5 mini、o4 mini high、GPT 4.5(按订阅层级可能被隐藏)。 // - 点击行为: // 点击快捷项会 setForce(model, 2000) 开启 2 秒的强制模型窗口,随后优先触发原生菜单项; // 若没有,则回退点击“重试/Regenerate”。期间 fetch 改写会把会话请求中的 model 重写为强制模型。 // // 触发与监听: // - 入口按钮:[data-testid="model-switcher-dropdown-button"]。首次点击时,对其关联菜单安装观察器; // 全局还注册一个 MutationObserver 作为兜底:发现任意新菜单后按类型路由到对应处理逻辑。 // - 其它:通过 Analytics 的“Model Switcher”事件同步顶部按钮文案;URL 的 ?model= 同步到按钮;样式上隐藏 legacy 分隔线。 // ---------------- 配置 ---------------- const TEST_ID_SWITCHER = 'model-switcher-dropdown-button'; // 订阅层级存储键与默认值 const SUB_KEY = 'chatgpt-subscription-tier'; const SUB_DEFAULT = 'plus'; const SUB_LEVELS = ['free','go','plus','team','edu','enterprise','pro']; function gmGet(key, defVal = undefined) { try { if (typeof GM_getValue === 'function') return GM_getValue(key, defVal); } catch {} try { const raw = localStorage.getItem(key); return raw == null ? defVal : JSON.parse(raw); } catch { return defVal; } } function gmSet(key, val) { try { if (typeof GM_setValue === 'function') return GM_setValue(key, val); } catch {} try { localStorage.setItem(key, JSON.stringify(val)); } catch {} } function getTier() { const saved = (gmGet(SUB_KEY, '') || '').toString().trim().toLowerCase(); return SUB_LEVELS.includes(saved) ? saved : ''; } function setTier(tier) { const norm = (tier || '').toString().trim().toLowerCase(); if (!SUB_LEVELS.includes(norm)) return; gmSet(SUB_KEY, norm); } function tierPromptText() { return '请选择订阅层级(仅出现一次,之后可在油猴菜单重新选择;不区分大小写):\n' + 'free / go / plus / team / edu / enterprise / pro\n\n' + '提示:若选错,可在油猴脚本菜单点击“重新选择订阅层级”。'; } function chooseTierInteractively(defaultTier = SUB_DEFAULT, silent = false) { while (true) { const inputRaw = window.prompt(tierPromptText(), defaultTier); if (inputRaw == null) { const chosen = (defaultTier || SUB_DEFAULT).toLowerCase(); setTier(chosen); if (!silent) { try { location.reload(); } catch {} } return chosen; } const input = String(inputRaw).trim().toLowerCase(); if (!SUB_LEVELS.includes(input)) { try { alert('订阅层级无效,请重新输入'); } catch {} continue; } setTier(input); if (!silent) { try { let msg = '已设置订阅层级:' + input; switch (input) { case 'edu': msg += '\n\n重要:edu 套餐的拓展模型如果无法在主页面使用,请前往 Projects 中使用。**且**用户必须在设置里开启“启用更多模型/老模型”。'; break; case 'plus': msg += '\n\n重要:必须在设置里开启“启用更多模型/老模型”,才能使用老模型'; break; case 'team': msg += '\n\n重要:必须让 team**所有者/管理者** 在设置中开启“启用更多模型/老模型”,**且**用户必须在设置里开启“启用更多模型/老模型”,才能使用老模型'; break; case 'enterprise': msg += '\n\n重要:必须让企业**所有者/管理者** 在设置中开启“启用更多模型/老模型”,**且**用户必须在设置里开启“启用更多模型/老模型”,才能使用老模型。拓展模型如果无法在主页面使用,请前往 Projects 中使用。'; break; case 'pro': msg += '\n\n重要:如果无法使用老模型,可以看下设置里有没有开启“启用更多模型/老模型”'; break; } alert(msg); } catch {} } if (!silent) { try { location.reload(); } catch {} } return input; } } function ensureTierChosen() { const t = getTier(); if (!t) return chooseTierInteractively(SUB_DEFAULT, false); return t; } // 注册“重新选择订阅层级”菜单 try { if (typeof GM_registerMenuCommand === 'function') { const currentTierForMenu = getTier() || '未设置'; GM_registerMenuCommand(`重新选择订阅层级(现在:${currentTierForMenu})`, () => chooseTierInteractively()); } } catch {} // 默认目标顺序(按 data-testid 后缀) const BASE_ORDER = [ 'gpt-5-thinking', 'gpt-5-t-mini', 'gpt-5-instant', 'gpt-5', 'gpt-5-mini', 'o3', 'o4-mini-high', 'o4-mini', 'gpt-4o', 'gpt-4-1', 'o3-pro', 'gpt-5-pro', 'gpt-4-5', ]; const PRO_PRIORITY_ORDER = [ 'gpt-5-thinking', 'gpt-4-5', 'o3', 'o4-mini-high', 'gpt-5-pro', 'o3-pro', ]; function getDesiredOrder() { const tier = getTier() || SUB_DEFAULT; if (tier === 'pro') { const seen = new Set(); const pushUnique = (arr, target) => { for (const id of arr) { if (seen.has(id)) continue; seen.add(id); target.push(id); } }; const result = []; pushUnique(PRO_PRIORITY_ORDER, result); pushUnique(BASE_ORDER, result); return result; } return BASE_ORDER; } const ALT_IDS = { 'gpt-4-1': ['gpt-4.1'], 'gpt-4-5': ['gpt-4.5'] }; // 点击后不自动收起菜单的模型(硬编码名单) const NO_CLOSE_ON_CHOOSE_IDS = new Set([ 'gpt-5', 'gpt-5-instant', 'gpt-5-thinking', 'gpt-5-pro', 'gpt-5-t-mini', ]); // 自定义模型项(若该菜单已经有官方同名项则不重复插入) const CUSTOM_MODELS = [ { id: 'o3', label: 'o3' }, { id: 'o3-pro', label: 'o3 pro' }, { id: 'gpt-4-1', label: 'GPT 4.1' }, { id: 'gpt-4o', label: 'GPT 4o' }, { id: 'o4-mini', label: 'o4 mini' }, { id: 'o4-mini-high', label: 'o4 mini high' }, { id: 'gpt-5', label: 'GPT 5 Auto' }, { id: 'gpt-5-instant',label: 'GPT 5 Instant' }, { id: 'gpt-5-t-mini', label: 'GPT 5 Thinking Mini' }, { id: 'gpt-5-mini', label: 'GPT 5 mini' }, { id: 'gpt-5-thinking', label: 'GPT 5 Thinking' }, { id: 'gpt-5-pro', label: 'GPT 5 Pro' }, { id: 'gpt-4-5', label: 'GPT 4.5' }, ]; // 层级可用性规则 function isModelAllowed(id) { const norm = normalizeModelId(id); const tier = getTier() || SUB_DEFAULT; if (tier === 'free' || tier === 'go') { return norm === 'gpt-5-t-mini' || norm === 'gpt-5' || norm === 'gpt-5-mini'; } if (tier === 'plus') { if (norm === 'o3-pro' || norm === 'gpt-5-pro' || norm === 'gpt-4-5') return false; return true; } // team 目前无 GPT 4.5 if (tier === 'team') { if (norm === 'gpt-4-5') return false; return true; } // edu / enterprise / pro 全量可用 return true; } // ---------------- 工具 ---------------- const debounce = (fn, wait = 50) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn.apply(null, a), wait); }; }; // 标准化与美化名称 const CUSTOM_NAME_MAP = new Map(CUSTOM_MODELS.map(m => [m.id.toLowerCase(), m.label])); const EXTRA_NAME_MAP = new Map(Object.entries({ 'gpt-4o': 'GPT 4o', 'gpt-4-1': 'GPT 4.1', 'gpt-4.1': 'GPT 4.1', 'gpt-4-5': 'GPT 4.5', 'o3': 'o3', 'o3-pro': 'o3 pro', 'o4-mini': 'o4 mini', 'o4-mini-high': 'o4 mini high', 'gpt-5': 'GPT 5 Auto', 'gpt-5-instant': 'GPT 5 Instant', 'gpt-5-t-mini': 'GPT 5 Thinking Mini', 'gpt-5-thinking': 'GPT 5 Thinking', 'gpt-5-pro': 'GPT 5 Pro', 'gpt-5-mini': 'GPT 5 mini', })); function normalizeModelId(id) { if (!id) return ''; return String(id).trim().toLowerCase().replace(/\s+/g, '-').replace(/\./g, '-'); } function prettyName(id) { const norm = normalizeModelId(id); return CUSTOM_NAME_MAP.get(norm) || EXTRA_NAME_MAP.get(norm) || id || ''; } function setAllSwitcherButtonsModel(modelId) { if (!modelId) return; const norm = normalizeModelId(modelId); const name = prettyName(norm); document.querySelectorAll(`[data-testid="${TEST_ID_SWITCHER}"]`).forEach((btn) => { const labelContainer = btn.querySelector('div, span'); if (labelContainer) { labelContainer.textContent = `ChatGPT ${name}`; labelContainer.style.color = 'var(--token-text-primary, var(--text-primary, inherit))'; } btn.setAttribute('aria-label', `Model selector, current model is ${norm}`); btn.dataset.currentModel = norm; }); } function updateAllSwitcherButtonsFromURL() { const url = new URL(window.location.href); const currentModel = url.searchParams.get('model'); if (!currentModel) return; setAllSwitcherButtonsModel(currentModel); } function findAssociatedMenu(triggerBtn) { const id = triggerBtn.getAttribute('id'); if (!id) return null; return document.querySelector(`[role="menu"][aria-labelledby="${CSS.escape(id)}"]`); } // 关闭(收起)与某按钮关联的 Main 模型选择器。 function closeMenu(menuEl) { try { const menu = menuEl && (menuEl.closest?.('[role="menu"], [role="listbox"], [data-radix-menu-content]') || menuEl); if (!menu || !(menu instanceof HTMLElement)) return false; const labeledBy = menu.getAttribute('aria-labelledby'); if (labeledBy) { const btn = document.getElementById(labeledBy); if (btn) { try { btn.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); } catch {} try { btn.click(); } catch {} return true; } } // 回退:发送 Escape 事件尝试关闭 Radix 下拉 const target = document.activeElement || menu; try { target.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true })); } catch {} try { target.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true })); } catch {} return true; } catch { return false; } } // 仅识别“Main 模型选择器”(排除包含 Auto/Instant/Thinking/Pro 的“使用其他模型重试的模型选择器”) // Main 菜单识别:有 Main 菜单签名 + 不包含重试关键字 function isOfficialModelMenu(menuEl) { if (!menuEl || !(menuEl instanceof HTMLElement)) return false; const role = menuEl.getAttribute('role'); if (role !== 'menu' && role !== 'listbox') return false; const items = Array.from(menuEl.querySelectorAll('[role="menuitem"], [data-radix-collection-item]')); const labels = items.map((el) => { const t = el.querySelector?.('.truncate'); const raw = (t?.textContent ?? el.textContent ?? '').trim(); return raw.split('\n')[0].trim(); }); const hasVariantMarker = labels.some((l) => /^(Auto|Instant|Thinking(?: mini)?|Pro|Ultra(?:\s*Think(?:ing)?)?)$/i.test(l)); if (hasVariantMarker) return false; const hasOfficialSignature = !!(menuEl.querySelector('[data-testid^="model-switcher-"]') || menuEl.querySelector('[data-cgpt-turn]')); if (!hasOfficialSignature) return false; return true; } // ---------------- 黏性重排 ---------------- // 黏性重排:把“我们关心的项”按照订阅层级对应的顺序, // 仅在不一致时最小化 DOM 变动地整体移动,避免 hover 抖动/菜单意外关闭。 const STICKY_REORDER = new WeakMap(); function findItemNode(menu, id) { let node = menu.querySelector(`[data-radix-collection-item][data-testid="model-switcher-${CSS.escape(id)}"]`) || menu.querySelector(`[data-testid="model-switcher-${CSS.escape(id)}"]`) || menu.querySelector(`[data-custom-model="${CSS.escape(id)}"]`); if (!node && ALT_IDS[id]) { for (const alt of ALT_IDS[id]) { node = menu.querySelector(`[data-testid="model-switcher-${CSS.escape(alt)}"]`) || menu.querySelector(`[data-custom-model="${CSS.escape(alt)}"]`); if (node) break; } } return node; } // 对 Main 模型选择器进行“黏性重排”(与 addCustomModels 配合)。 function applyDesiredOrder(menu) { // 1) 收集期望顺序中、当前实际存在于该菜单且“当前层级允许”的“顶层项”节点 const desiredNodes = []; const seen = new Set(); const desiredOrder = getDesiredOrder(); for (const id of desiredOrder) { if (!isModelAllowed(id)) continue; let n = findItemNode(menu, id); if (!n) continue; // 升到以 menu 为直接父级的顶层容器,避免移动子层导致 hover 抖动 while (n && n.parentElement && n.parentElement !== menu) n = n.parentElement; if (!n || seen.has(n)) continue; seen.add(n); desiredNodes.push(n); } if (desiredNodes.length === 0) return; // 2) 取当前顺序:按 menu.children 顺序过滤出我们关心的节点 const current = Array.from(menu.children).filter(ch => seen.has(ch)); // 3) 若顺序已匹配,则不做任何 DOM 变动(避免 pointerleave/blur 导致菜单关闭) const sameOrder = current.length === desiredNodes.length && current.every((n, i) => n === desiredNodes[i]); if (sameOrder) return; // 4) 仅在不一致时才整体移动,以最小化变更次数 const frag = document.createDocumentFragment(); desiredNodes.forEach(n => frag.appendChild(n)); menu.appendChild(frag); } // UI 微调:压缩 GPT‑5 系列二行描述、统一标题、隐藏“Legacy models”入口和相关分隔线。 function normalizeMenuUI(menu) { try { // 压缩 GPT‑5 系列项:去除第二行描述 const g5 = menu.querySelectorAll('[data-testid^="model-switcher-gpt-5"], [data-radix-collection-item][data-testid^="model-switcher-gpt-5"]'); g5.forEach((el) => { const container = el.querySelector('.min-w-0'); if (!container) return; const children = Array.from(container.children); children.forEach((node, idx) => { if (idx >= 1 && node.tagName === 'DIV') node.remove(); }); }); // 标题规范化 const rename = (key, text) => { const n = menu.querySelector(`[data-radix-collection-item][data-testid="model-switcher-${key}"] .min-w-0 span`) || menu.querySelector(`[data-testid="model-switcher-${key}"] .min-w-0 span`); if (n) n.textContent = text; }; rename('gpt-5', 'GPT 5 Auto'); rename('gpt-5-instant', 'GPT 5 Instant'); rename('gpt-5-t-mini', 'GPT 5 Thinking Mini'); rename('gpt-5-mini', 'GPT 5 mini'); rename('gpt-5-thinking', 'GPT 5 Thinking'); rename('gpt-5-pro', 'GPT 5 Pro'); // 隐藏 Legacy models 子菜单入口 const toHide = new Set(); const exact = menu.querySelector('[data-testid="Legacy models-submenu"]'); if (exact) toHide.add(exact); menu.querySelectorAll('[role="menuitem"][data-has-submenu]').forEach((el) => { const txt = (el.textContent || '').toLowerCase(); const tid = (el.getAttribute('data-testid') || '').toLowerCase(); if (txt.includes('legacy models') || tid.includes('legacy models')) toHide.add(el); }); toHide.forEach((el) => { el.style.display = 'none'; el.setAttribute('data-ext-hidden','1'); }); // 隐藏“GPT-5”分组标题与紧随的分隔线 menu.querySelectorAll('div.__menu-label.mb-0').forEach((el) => { const t = (el.textContent || '').trim(); if (t === 'GPT-5') { el.style.display = 'none'; el.setAttribute('data-ext-hidden','1'); const sep = el.nextElementSibling; if (sep && sep.getAttribute('role') === 'separator') { sep.style.display = 'none'; sep.setAttribute('data-ext-hidden','1'); } } }); // 保险:具有这些类名的分隔线也隐藏 menu.querySelectorAll('[role="separator"].bg-token-border-default.h-px.mx-4.my-1').forEach((el) => { el.style.display = 'none'; el.setAttribute('data-ext-hidden','1'); }); } catch {} } // 按订阅层级隐藏/显示菜单项(官方项与自定义项均处理) function syncMenuByTier(menu) { try { // 遍历当前菜单中所有“模型项”(官方或自定义) const allItems = Array.from(menu.querySelectorAll('[data-radix-collection-item][data-testid^="model-switcher-"], [data-testid^="model-switcher-"]')); for (const el of allItems) { const testid = el.getAttribute('data-testid') || ''; const m = /^model-switcher-(.+)$/.exec(testid); const id = m ? normalizeModelId(m[1]) : ''; if (!id) continue; // 升为顶层节点 let n = el; while (n && n.parentElement && n.parentElement !== menu) n = n.parentElement; const allowed = isModelAllowed(id); if (n && n instanceof HTMLElement) { if (allowed) { if (n.dataset.extTierHidden === '1') { n.style.display = ''; delete n.dataset.extTierHidden; } } else { n.style.display = 'none'; n.dataset.extTierHidden = '1'; } } } } catch {} } function ensureStickyReorder(menu) { if (!menu || STICKY_REORDER.has(menu)) return; let scheduled = false; const schedule = () => { if (scheduled) return; scheduled = true; requestAnimationFrame(() => { scheduled = false; try { normalizeMenuUI(menu); } catch {} try { syncMenuByTier(menu); } catch {} try { applyDesiredOrder(menu); } catch {} }); }; const mo = new MutationObserver((muts) => { for (const m of muts) { if (m.type === 'childList') { schedule(); break; } } }); mo.observe(menu, { childList: true }); STICKY_REORDER.set(menu, mo); schedule(); // 首次也排一次 } // ---------------- 自定义项:原生风格 + 丝滑选择 ---------------- // 丝滑选择: // 1) 立即更新 URL 中的 ?model= 和顶部按钮文案(无闪烁), // 2) requestAnimationFrame 后尝试点击同 id 的官方项,让后端同步切换。 function selectModelQuick(id) { // 1) 立即更新 URL 和按钮文案(丝滑) if (!isModelAllowed(id)) { try { alert('当前订阅层级不可使用该模型'); } catch {} return; } try { const url = new URL(window.location.href); url.searchParams.set('model', id); history.pushState({}, '', url.toString()); try { window.dispatchEvent(new Event('pushstate')); } catch {} try { window.dispatchEvent(new Event('locationchange')); } catch {} try { window.dispatchEvent(new PopStateEvent('popstate')); } catch {} setAllSwitcherButtonsModel(id); } catch {} // 2) 联动点官方同 id 项(让后端状态也切换) const sel = `[data-radix-collection-item][data-testid="model-switcher-${CSS.escape(id)}"]:not([data-ext-custom])`; const tryClick = () => { const el = document.querySelector(sel); if (!el) return false; el.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); el.click(); return true; }; requestAnimationFrame(() => { if (!tryClick()) setTimeout(tryClick, 120); }); } function createNativeLikeCustomItem(id, label) { const item = document.createElement('div'); item.setAttribute('role','menuitem'); item.setAttribute('tabindex','0'); item.className = 'group __menu-item'; item.setAttribute('data-radix-collection-item',''); item.setAttribute('data-orientation','vertical'); item.dataset.testid = `model-switcher-${id}`; // data-testid(保持一致,以便排序匹配) item.setAttribute('data-custom-model', id); item.setAttribute('data-ext-custom','1'); // 防止被“点官方项”逻辑误点 item.innerHTML = `