您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Bug Team,好用、爱用 ♥
// ==UserScript== // @name Switch Bug Team Model // @namespace http://tampermonkey.net/ // @version 1.2 // @description Bug Team,好用、爱用 ♥ // @author wandouyu // @match *://chat.voct.dev/* // @match *://chatgpt.com/* // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; let dropdownElement = null; let isDropdownVisible = false; let insertionAttempted = false; const SCRIPT_PREFIX = "模型切换器:"; const DEBOUNCE_DELAY = 300; const modelMap = { "o3 ": "o3", "o4-mini-high": "o4-mini-high", "o4-mini": "o4-mini", "gpt-4.5": "gpt-4-5", "gpt-4o": "gpt-4o", "gpt-4o-mini": "gpt-4o-mini", "gpt-4o (tasks)": "gpt-4o-jawbone", "gpt-4": "gpt-4" }; const modelDisplayNames = Object.keys(modelMap); const modelIds = Object.values(modelMap); GM_addStyle(` .model-switcher-container { position: relative; display: inline-block; margin-left: 4px; margin-right: 4px; align-self: center; } #model-switcher-button { display: inline-flex; align-items: center; justify-content: center; height: 36px; min-width: 36px; padding: 0 12px; border-radius: 9999px; border: 1px solid var(--token-border-light, #E5E5E5); font-size: 14px; font-weight: 500; color: var(--token-text-secondary, #666666); background-color: var(--token-main-surface-primary, #FFFFFF); cursor: pointer; white-space: nowrap; transition: background-color 0.2s ease; box-sizing: border-box; } #model-switcher-button:hover { background-color: var(--token-main-surface-secondary, #F7F7F8); } #model-switcher-dropdown { position: fixed; display: block; background-color: var(--token-main-surface-primary, white); border: 1px solid var(--token-border-medium, #E5E5E5); border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); z-index: 1050; min-width: 180px; overflow-y: auto; padding: 4px 0; } .model-switcher-item { display: block; padding: 8px 16px; color: var(--token-text-primary, #171717); text-decoration: none; white-space: nowrap; cursor: pointer; font-size: 14px; } .model-switcher-item:hover { background-color: var(--token-main-surface-secondary, #F7F7F8); } .model-switcher-item.active { font-weight: bold; } `); function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function getCurrentModelInfo() { const params = new URLSearchParams(window.location.search); const currentModelIdFromUrl = params.get('model'); let effectiveModelId = modelIds[0]; let currentIndex = 0; if (currentModelIdFromUrl) { const foundIndex = modelIds.indexOf(currentModelIdFromUrl); if (foundIndex !== -1) { effectiveModelId = currentModelIdFromUrl; currentIndex = foundIndex; } } const currentDisplayName = modelDisplayNames[currentIndex]; return { currentId: effectiveModelId, displayName: currentDisplayName, index: currentIndex }; } function createModelSwitcher() { const container = document.createElement('div'); container.className = 'model-switcher-container'; container.id = 'model-switcher-container'; const button = document.createElement('button'); button.id = 'model-switcher-button'; button.type = 'button'; const dropdown = document.createElement('div'); dropdown.className = 'model-switcher-dropdown'; dropdown.id = 'model-switcher-dropdown'; const initialModelInfo = getCurrentModelInfo(); button.textContent = initialModelInfo.displayName; modelDisplayNames.forEach((name, index) => { const modelId = modelIds[index]; const item = document.createElement('a'); item.className = 'model-switcher-item'; item.textContent = name; item.dataset.modelId = modelId; item.href = '#'; if (initialModelInfo.currentId === modelId) { item.classList.add('active'); } item.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const selectedModelId = e.target.dataset.modelId; const currentModelIdNow = getCurrentModelInfo().currentId; if (currentModelIdNow !== selectedModelId) { const url = new URL(window.location.href); url.searchParams.set('model', selectedModelId); console.log(`${SCRIPT_PREFIX} 切换目标: ${name} (${selectedModelId})`); window.location.href = url.toString(); // hideDropdown(); } else { console.log(`${SCRIPT_PREFIX} 模型无需切换`); hideDropdown(); } }); dropdown.appendChild(item); }); button.addEventListener('click', (e) => { e.stopPropagation(); toggleDropdown(); }); container.appendChild(button); dropdownElement = dropdown; return container; } function showDropdown() { if (!dropdownElement || isDropdownVisible) return; const button = document.getElementById('model-switcher-button'); if (!button) { console.error(`${SCRIPT_PREFIX} 找不到添加的模型切换器按钮,无法定位下拉菜单。`); return; } if (!dropdownElement.parentElement) { document.body.appendChild(dropdownElement); } isDropdownVisible = true; const buttonRect = button.getBoundingClientRect(); const dropdownHeight = dropdownElement.offsetHeight; const dropdownWidth = dropdownElement.offsetWidth; const spaceAbove = buttonRect.top; const spaceBelow = window.innerHeight - buttonRect.bottom; const margin = 5; let top, left = buttonRect.left; if (spaceBelow >= dropdownHeight + margin || spaceBelow >= spaceAbove) { top = buttonRect.bottom + margin; } else { top = buttonRect.top - dropdownHeight - margin; } top = Math.max(margin, Math.min(top, window.innerHeight - dropdownHeight - margin)); left = Math.max(margin, Math.min(left, window.innerWidth - dropdownWidth - margin)); dropdownElement.style.top = `${top}px`; dropdownElement.style.left = `${left}px`; document.addEventListener('click', handleClickOutside, true); window.addEventListener('resize', debouncedHideDropdown); window.addEventListener('scroll', debouncedHideDropdownOnScroll, true); } function hideDropdown() { if (!dropdownElement || !isDropdownVisible) return; if (dropdownElement.parentElement) { dropdownElement.remove(); } isDropdownVisible = false; document.removeEventListener('click', handleClickOutside, true); window.removeEventListener('resize', debouncedHideDropdown); window.removeEventListener('scroll', debouncedHideDropdownOnScroll, true); } const debouncedHideDropdown = debounce(hideDropdown, 150); const debouncedHideDropdownOnScroll = debounce(hideDropdown, 150); function toggleDropdown() { if (isDropdownVisible) { hideDropdown(); } else { showDropdown(); } } function handleClickOutside(event) { const button = document.getElementById('model-switcher-button'); if (dropdownElement && dropdownElement.parentNode && button && !button.contains(event.target) && !dropdownElement.contains(event.target)) { hideDropdown(); } } function updateSwitcherState() { const button = document.getElementById('model-switcher-button'); if (!button) return; const currentInfo = getCurrentModelInfo(); if (button.textContent !== currentInfo.displayName) { button.textContent = currentInfo.displayName; console.log(`${SCRIPT_PREFIX} 按钮文本更新为: ${currentInfo.displayName}`); } if (dropdownElement) { const items = dropdownElement.querySelectorAll('.model-switcher-item'); let changed = false; items.forEach((item) => { const modelId = item.dataset.modelId; const shouldBeActive = (currentInfo.currentId === modelId); if (item.classList.contains('active') !== shouldBeActive) { if (shouldBeActive) { item.classList.add('active'); } else { item.classList.remove('active'); } changed = true; } }); if (changed) { console.log(`${SCRIPT_PREFIX} 下拉菜单激活状态已更新`); } } } function checkAndInsertSwitcher() { const composerSelector = 'form[data-type="unified-composer"]'; const toolbarContainer = document.querySelector(composerSelector); if (!toolbarContainer) { const existingSwitcher = document.getElementById('model-switcher-container'); if (existingSwitcher && !existingSwitcher.closest(composerSelector)) { console.log(`${SCRIPT_PREFIX} 切换器存在于预期容器之外,移除。`); existingSwitcher.remove(); insertionAttempted = false; hideDropdown(); } return; } const existingSwitcher = document.getElementById('model-switcher-container'); const targetSelector = 'div[style*="--vt-composer-attach-file-action"]'; const insertionTarget = toolbarContainer.querySelector(targetSelector); if (existingSwitcher) { if (!toolbarContainer.contains(existingSwitcher)) { console.log(`${SCRIPT_PREFIX} 切换器存在但不在预期容器内,移动它...`); if (insertionTarget) { insertionTarget.insertAdjacentElement('afterend', existingSwitcher); } else { toolbarContainer.appendChild(existingSwitcher); console.log(`${SCRIPT_PREFIX} 未找到附件按钮,切换器移动到工具栏末尾`); } } updateSwitcherState(); insertionAttempted = true; } else { if (insertionTarget) { console.log(`${SCRIPT_PREFIX} 找到插入点 (${targetSelector}),尝试插入切换器...`); const switcherContainer = createModelSwitcher(); insertionTarget.insertAdjacentElement('afterend', switcherContainer); console.log(`${SCRIPT_PREFIX} 成功插入至附件上传按钮之后`); insertionAttempted = true; } else { console.warn(`${SCRIPT_PREFIX} 在工具栏内未找到附件上传按钮 (${targetSelector}),尝试添加到工具栏末尾`); const switcherContainer = createModelSwitcher(); toolbarContainer.appendChild(switcherContainer); console.log(`${SCRIPT_PREFIX} 成功插入至工具栏末尾`); insertionAttempted = true; } } const currentSwitcherExists = !!document.getElementById('model-switcher-container'); if (insertionAttempted && !currentSwitcherExists) { console.log(`${SCRIPT_PREFIX} 切换器容器似乎已被移除, 将在下次检查时尝试重新插入`); insertionAttempted = false; hideDropdown(); } } const debouncedCheckAndInsert = debounce(checkAndInsertSwitcher, DEBOUNCE_DELAY); const observer = new MutationObserver((mutationsList, obs) => { debouncedCheckAndInsert(); }); console.log(`${SCRIPT_PREFIX} 正在监控 DOM 变化`); observer.observe(document.body, { childList: true, subtree: true }); console.log(`${SCRIPT_PREFIX} 计划在 500ms 后进行首次检查`); setTimeout(debouncedCheckAndInsert, 500); })();