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