ChatGPT 模型切换助手: 4o-mini、o4-mini、o3 等更多...

让您在对话中随意切换语言模型,并用不同颜色标示生成回应的语言模型

当前为 2025-05-29 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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