您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
让您在对话中随意切换语言模型,并用不同颜色标示生成回应的语言模型
// ==UserScript== // @name ChatGPT Realtime Model Switcher: 4o-mini, o4-mini, o3 and more! // @name:zh-CN ChatGPT 模型切换助手: 4o-mini、o4-mini、o3 等更多... // @name:zh-TW ChatGPT 模型切換助手: 4o-mini、o4-mini、o3 等更多... // @namespace http://tampermonkey.net/ // @version 0.54.1 // @description Allowing you to switch models during a single conversation, and highlight responses by color based on the model generating them // @description:zh-CN 让您在对话中随意切换语言模型,并用不同颜色标示生成回应的语言模型 // @description:zh-TW 讓您在對話中隨意切換語言模型,並用不同顏色標示生成回答的語言模型 // @match *://chatgpt.com/* // @author d0gkiller87 // @license MIT // @grant unsafeWindow // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM_registerMenuCommand // @grant GM.registerMenuCommand // @grant GM.unregisterMenuCommand // @run-at document-idle // @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com // ==/UserScript== (async function() { 'use strict'; function injectStyle( style, isDisabled = false ) { const styleNode = document.createElement( 'style' ); styleNode.type = 'text/css'; styleNode.textContent = style; document.head.appendChild( styleNode ); styleNode.disabled = isDisabled; return styleNode; } const PlanType = Object.freeze({ free: 0, plus: 1, pro : 2 }); class ModelSwitcher { getPlanType() { for ( const scriptNode of document.querySelectorAll( 'script' ) ) { let match; while ( ( match = /\\"planType\\"\s*,\s*\\"(\w+?)\\"/.exec( scriptNode.innerHTML ) ) !== null ) { return match[1]; } } return 'free' } async init() { this.model = await GM.getValue( 'model', 'auto' ); this.buttons = {}; this.offsetX = 0; this.offsetY = 0; this.isDragging = false; this.shouldCancelClick = false; this.modelSelector = null; this.isMenuVisible = await GM.getValue( 'isMenuVisible', true ); this.isMenuVisibleCommandId = null; this.modelHighlightStyleNode = null; this.isModelHighlightEnabled = await GM.getValue( 'isModelHighlightEnabled', true ); this.isModelHighlightEnabledCommandId = null; this.isMenuVertical = await GM.getValue( 'isMenuVertical', true ); this.isMenuVerticalCommandId = null; this.conversationUrlRegex = new RegExp( /https:\/\/chatgpt\.com\/backend-api\/.*conversation/ ); const planType = PlanType[ this.getPlanType() ]; const models = [ // [ PlanType.pro, "o1", "o1" ], // retired [ PlanType.pro, "o1-pro", "o1-pro" ], // [ PlanType.free, "o3-mini", "o3-mini" ], // retired [ PlanType.plus, "o3", "o3" ], [ PlanType.free, "o4-mini", "o4-mini" ], [ PlanType.plus, "o4-mini-high", "o4-mini-high" ], [ PlanType.free, "gpt-3.5", "gpt-3-5" ], [ PlanType.free, "4o-mini", "gpt-4o-mini" ], [ PlanType.free, "4.1-mini", "gpt-4-1-mini" ], // [ PlanType.free, "gpt-4", "gpt-4" ], // same as 4o [ PlanType.free, "gpt-4o", "gpt-4o" ], [ PlanType.plus, "gpt-4.1", "gpt-4-1" ], // [ PlanType.plus, "4o-jawbone", "4o-jawbone" ], // retired (https://x.com/testingcatalog/status/1915483050953125965) [ PlanType.plus, "gpt-4.5", "gpt-4-5" ], [ PlanType.free, "default", "auto" ], ]; this.availableModels = {}; for ( const [ minimumPlan, modelName, modelValue ] of models ) { if ( planType >= minimumPlan ) { this.availableModels[modelName] = modelValue; } } } hookFetch() { const originalFetch = unsafeWindow.fetch; unsafeWindow.fetch = async ( resource, config = {} ) => { if ( typeof resource === 'string' && resource.match( this.conversationUrlRegex ) && config.method === 'POST' && config.headers && config.headers['Content-Type'] === 'application/json' && config.body && this.model !== 'auto' ) { const body = JSON.parse( config.body ); body.model = this.model; config.body = JSON.stringify( body ); } return originalFetch( resource, config ); }; } injectToggleButtonStyle() { let style = ` :root { color-scheme: light dark; } #model-selector { position: absolute; display: flex; flex-direction: column; gap: 6px; cursor: grab; } #model-selector.horizontal { flex-direction: row; } #model-selector.hidden { display: none; } #model-selector button { background: none; border: 1px solid light-dark(#151515, white); color: light-dark(#151515, white); padding: 6px; cursor: pointer; font-size: 0.9rem; user-select: none; } #model-selector button.selected { color: light-dark(white, white); } :root { --o1-pro-color: 139, 232, 27; --o3-color: 139, 232, 27; --gpt-3-5-color: 0, 106, 129; --gpt-4-1-color: 13, 121, 255; --gpt-4-5-color: 126, 3, 165; --gpt-4o-color: 18, 45, 134; --o4-mini-high-color: 176, 53, 0; --o4-mini-color: 203, 91, 0; --gpt-4o-jawbone-color: 201, 42, 42; --gpt-4o-mini-color: 67, 162, 90; --gpt-4-1-mini-color: 117, 166, 12; --auto-color: 131, 131, 139; --unknown-model-btn-color: 67, 162, 90; --unknown-model-box-shadow-color: 48, 255, 19; } `; for ( const model of Object.values( this.availableModels ) ) { style += ` #model-selector button.btn-${ model } { background-color: rgb(var(--${ model }-color, var(--unknown-model-btn-color))); } `; } injectStyle( style ); } refreshButtons() { for ( const [ model, button ] of Object.entries( this.buttons ) ) { const isSelected = model === `btn-${ this.model }`; button.classList.toggle( model, isSelected ); button.classList.toggle( 'selected', isSelected ); } } async reloadMenuVisibleToggle() { this.isMenuVisibleCommandId = await GM.registerMenuCommand( `${ this.isMenuVisible ? '☑︎' : '☐' } Show model selector`, async () => { this.isMenuVisible = !this.isMenuVisible; await GM.setValue( 'isMenuVisible', this.isMenuVisible ); this.modelSelector.classList.toggle( 'hidden', !this.isMenuVisible ); this.reloadMenuVisibleToggle(); }, this.isMenuVisibleCommandId ? { id: this.isMenuVisibleCommandId } : {} ); } async reloadMenuVerticalToggle() { this.isMenuVerticalCommandId = await GM.registerMenuCommand( `┖ Style: ${ this.isMenuVertical ? 'vertical ↕' : 'horizontal ↔' }`, async () => { this.isMenuVertical = !this.isMenuVertical; await GM.setValue( 'isMenuVertical', this.isMenuVertical ); const originalRight = parseInt( this.modelSelector.style.left ) + this.modelSelector.offsetWidth; const originalBottom = parseInt( this.modelSelector.style.top ) + this.modelSelector.offsetHeight; this.modelSelector.style.visibility = 'hidden'; this.modelSelector.style.left = '0px'; this.modelSelector.style.top = '0px'; this.modelSelector.classList.toggle( 'horizontal', !this.isMenuVertical ); this.modelSelector.style.left = `${ originalRight - this.modelSelector.offsetWidth }px`; this.modelSelector.style.top = `${ originalBottom - this.modelSelector.offsetHeight }px`; this.modelSelector.style.visibility = 'visible'; await GM.setValue( 'relativeMenuPosition', this.getCurrentRelativeMenuPosition() ); this.reloadMenuVerticalToggle(); }, this.isMenuVerticalCommandId ? { id: this.isMenuVerticalCommandId } : {} ); } injectMessageModelHighlightStyle() { let style = ` div[data-message-model-slug] { padding: 0px 5px; box-shadow: 0 0 3px 3px rgba(var(--unknown-model-box-shadow-color), 0.65); } `; for ( const model of Object.values( this.availableModels ) ) { style += ` div[data-message-model-slug="${ model }"] { box-shadow: 0 0 3px 3px rgba(var(--${ model }-color, var(--unknown-model-box-shadow-color)), 0.8); } `; } this.modelHighlightStyleNode = injectStyle( style, !this.isModelHighlightEnabled ); } async reloadMessageModelHighlightToggle() { this.isModelHighlightEnabledCommandId = await GM.registerMenuCommand( `${ this.isModelHighlightEnabled ? '☑︎' : '☐' } Show model identifer`, async () => { this.isModelHighlightEnabled = !this.isModelHighlightEnabled; await GM.setValue( 'isModelHighlightEnabled', this.isModelHighlightEnabled ); this.modelHighlightStyleNode.disabled = !this.isModelHighlightEnabled; this.reloadMessageModelHighlightToggle(); }, this.isModelHighlightEnabledCommandId ? { id: this.isModelHighlightEnabledCommandId } : {} ); } createModelSelectorMenu() { this.modelSelector = document.createElement( 'div' ); this.modelSelector.id = 'model-selector'; for ( const [ modelName, modelValue ] of Object.entries( this.availableModels ) ) { const button = document.createElement( 'button' ); button.textContent = modelName; button.title = modelValue; button.addEventListener( 'click', async event => { if ( this.shouldCancelClick ) { event.preventDefault(); event.stopImmediatePropagation(); return; } this.model = modelValue; await GM.setValue( 'model', modelValue ); this.refreshButtons(); } ); this.modelSelector.appendChild( button ); this.buttons[`btn-${ modelValue }`] = button; } this.modelSelector.classList.toggle( 'hidden', !this.isMenuVisible ); this.modelSelector.classList.toggle( 'horizontal', !this.isMenuVertical ); return this.modelSelector; } injectMenu() { document.body.appendChild( this.modelSelector ); } monitorBodyChanges() { const observer = new MutationObserver( mutationsList => { for ( const mutation of mutationsList ) { if ( document.body.querySelector( '#model-selector' ) ) continue; this.injectMenu(); break; } }); observer.observe( document.body, { childList: true } ); } getDefaultRelativeMenuPosition() { return { offsetRight: 33, offsetBottom: 36 }; } relativeToAbsolutePosition( relativeMenuPosition ) { return { left: `${ window.innerWidth - this.modelSelector.offsetWidth - relativeMenuPosition.offsetRight }px`, top: `${ window.innerHeight - this.modelSelector.offsetHeight - relativeMenuPosition.offsetBottom }px` } } getCurrentRelativeMenuPosition() { return { offsetRight: window.innerWidth - parseInt( this.modelSelector.style.left ) - this.modelSelector.offsetWidth, offsetBottom: window.innerHeight - parseInt( this.modelSelector.style.top ) - this.modelSelector.offsetHeight } } async restoreMenuPosition() { const menuPosition = await GM.getValue( 'menuPosition', null ); // <= v0.53.1 migration if ( menuPosition ) { this.modelSelector.style.left = menuPosition.left; this.modelSelector.style.top = menuPosition.top; await GM.setValue( 'relativeMenuPosition', { offsetRight: window.innerWidth - parseInt( menuPosition.left ) - this.modelSelector.offsetWidth, offsetBottom: window.innerHeight - parseInt( menuPosition.top ) - this.modelSelector.offsetHeight } ); await GM.deleteValue( 'menuPosition' ); } else { const relativeMenuPosition = await GM.getValue( 'relativeMenuPosition', this.getDefaultRelativeMenuPosition() ); const absoluteMenuPosition = this.relativeToAbsolutePosition( relativeMenuPosition ); this.modelSelector.style.left = absoluteMenuPosition.left; this.modelSelector.style.top = absoluteMenuPosition.top; } } monitorWindowResize() { window.addEventListener( 'resize', async event => { const relativeMenuPosition = await GM.getValue( 'relativeMenuPosition', this.getDefaultRelativeMenuPosition() ); const absoluteMenuPosition = this.relativeToAbsolutePosition( relativeMenuPosition ); this.modelSelector.style.left = absoluteMenuPosition.left; this.modelSelector.style.top = absoluteMenuPosition.top; } ); } async registerResetMenuPositionCommand() { await GM.registerMenuCommand( '⟲ Reset menu position', async () => { const defaultRelativeMenuPosition = this.getDefaultRelativeMenuPosition(); const defaultAbsoluteMenuPosition = this.relativeToAbsolutePosition( defaultRelativeMenuPosition ); this.modelSelector.style.left = defaultAbsoluteMenuPosition.left; this.modelSelector.style.top = defaultAbsoluteMenuPosition.top; await GM.setValue( 'relativeMenuPosition', defaultRelativeMenuPosition ); } ); } getPoint( event ) { return event.touches ? event.touches[0] : event; } mouseDownHandler( event ) { const point = this.getPoint( event ); this.offsetX = point.clientX - this.modelSelector.offsetLeft; this.offsetY = point.clientY - this.modelSelector.offsetTop; this.isDragging = true; this.shouldCancelClick = false; this.modelSelector.style.cursor = 'grabbing'; } mouseMoveHandler( event ) { if ( !this.isDragging ) return; const point = this.getPoint( event ); const oldLeft = this.modelSelector.style.left; const oldTop = this.modelSelector.style.top; this.modelSelector.style.left = ( point.clientX - this.offsetX ) + 'px'; this.modelSelector.style.top = ( point.clientY - this.offsetY ) + 'px'; if ( !this.shouldCancelClick && ( this.modelSelector.style.left != oldLeft || this.modelSelector.style.top != oldTop ) ) { this.shouldCancelClick = true; } // Prevent scrolling on touch if ( event.cancelable ) event.preventDefault(); } async mouseUpHandler( event ) { this.isDragging = false; this.modelSelector.style.cursor = 'grab'; document.body.style.userSelect = ''; await GM.setValue( 'relativeMenuPosition', this.getCurrentRelativeMenuPosition() ); } registerGrabbing() { // Mouse this.modelSelector.addEventListener( 'mousedown', this.mouseDownHandler.bind( this ) ); document.addEventListener( 'mousemove', this.mouseMoveHandler.bind( this ) ); document.addEventListener( 'mouseup', this.mouseUpHandler.bind( this ) ); // Touch this.modelSelector.addEventListener( 'touchstart', this.mouseDownHandler.bind( this ), { passive: false } ); document.addEventListener( 'touchmove', this.mouseMoveHandler.bind( this ), { passive: false } ); document.addEventListener( 'touchend', this.mouseUpHandler.bind( this ) ); } } const switcher = new ModelSwitcher(); await switcher.init(); switcher.hookFetch(); switcher.injectToggleButtonStyle(); switcher.injectMessageModelHighlightStyle(); switcher.createModelSelectorMenu(); await switcher.registerResetMenuPositionCommand(); await switcher.reloadMenuVisibleToggle(); await switcher.reloadMenuVerticalToggle(); await switcher.reloadMessageModelHighlightToggle(); switcher.refreshButtons(); switcher.monitorBodyChanges(); switcher.injectMenu(); await switcher.restoreMenuPosition(); switcher.monitorWindowResize(); switcher.registerGrabbing(); })();