Switch Bug Team Model

Bug Team,好用、爱用 ♥

  1. // ==UserScript==
  2. // @name Switch Bug Team Model
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Bug Team,好用、爱用 ♥
  6. // @author wandouyu
  7. // @match *://chat.voct.dev/*
  8. // @match *://chatgpt.com/*
  9. // @grant GM_addStyle
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. let dropdownElement = null;
  17. let isDropdownVisible = false;
  18. let insertionAttempted = false;
  19. const SCRIPT_PREFIX = "模型切换器:";
  20. const DEBOUNCE_DELAY = 300;
  21. const modelMap = {
  22. "o3 ": "o3",
  23. "o4-mini-high": "o4-mini-high",
  24. "o4-mini": "o4-mini",
  25. "gpt-4.5": "gpt-4-5",
  26. "gpt-4o": "gpt-4o",
  27. "gpt-4o-mini": "gpt-4o-mini",
  28. "gpt-4o (tasks)": "gpt-4o-jawbone",
  29. "gpt-4": "gpt-4"
  30. };
  31. const modelDisplayNames = Object.keys(modelMap);
  32. const modelIds = Object.values(modelMap);
  33.  
  34. GM_addStyle(`
  35. .model-switcher-container {
  36. position: relative;
  37. display: inline-block;
  38. margin-left: 4px;
  39. margin-right: 4px;
  40. align-self: center;
  41. }
  42.  
  43. #model-switcher-button {
  44. display: inline-flex;
  45. align-items: center;
  46. justify-content: center;
  47. height: 36px;
  48. min-width: 36px;
  49. padding: 0 12px;
  50. border-radius: 9999px;
  51. border: 1px solid var(--token-border-light, #E5E5E5);
  52. font-size: 14px;
  53. font-weight: 500;
  54. color: var(--token-text-secondary, #666666);
  55. background-color: var(--token-main-surface-primary, #FFFFFF);
  56. cursor: pointer;
  57. white-space: nowrap;
  58. transition: background-color 0.2s ease;
  59. box-sizing: border-box;
  60. }
  61.  
  62. #model-switcher-button:hover {
  63. background-color: var(--token-main-surface-secondary, #F7F7F8);
  64. }
  65.  
  66. #model-switcher-dropdown {
  67. position: fixed;
  68. display: block;
  69. background-color: var(--token-main-surface-primary, white);
  70. border: 1px solid var(--token-border-medium, #E5E5E5);
  71. border-radius: 8px;
  72. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  73. z-index: 1050;
  74. min-width: 180px;
  75. overflow-y: auto;
  76. padding: 4px 0;
  77. }
  78.  
  79. .model-switcher-item {
  80. display: block;
  81. padding: 8px 16px;
  82. color: var(--token-text-primary, #171717);
  83. text-decoration: none;
  84. white-space: nowrap;
  85. cursor: pointer;
  86. font-size: 14px;
  87. }
  88.  
  89. .model-switcher-item:hover {
  90. background-color: var(--token-main-surface-secondary, #F7F7F8);
  91. }
  92.  
  93. .model-switcher-item.active {
  94. font-weight: bold;
  95. }
  96. `);
  97.  
  98. function debounce(func, wait) {
  99. let timeout;
  100. return function executedFunction(...args) {
  101. const later = () => {
  102. clearTimeout(timeout);
  103. func(...args);
  104. };
  105. clearTimeout(timeout);
  106. timeout = setTimeout(later, wait);
  107. };
  108. }
  109.  
  110. function getCurrentModelInfo() {
  111. const params = new URLSearchParams(window.location.search);
  112. const currentModelIdFromUrl = params.get('model');
  113.  
  114. let effectiveModelId = modelIds[0];
  115. let currentIndex = 0;
  116.  
  117. if (currentModelIdFromUrl) {
  118. const foundIndex = modelIds.indexOf(currentModelIdFromUrl);
  119. if (foundIndex !== -1) {
  120. effectiveModelId = currentModelIdFromUrl;
  121. currentIndex = foundIndex;
  122. }
  123. }
  124.  
  125. const currentDisplayName = modelDisplayNames[currentIndex];
  126. return { currentId: effectiveModelId, displayName: currentDisplayName, index: currentIndex };
  127. }
  128.  
  129. function createModelSwitcher() {
  130. const container = document.createElement('div');
  131. container.className = 'model-switcher-container';
  132. container.id = 'model-switcher-container';
  133.  
  134. const button = document.createElement('button');
  135. button.id = 'model-switcher-button';
  136. button.type = 'button';
  137.  
  138. const dropdown = document.createElement('div');
  139. dropdown.className = 'model-switcher-dropdown';
  140. dropdown.id = 'model-switcher-dropdown';
  141.  
  142. const initialModelInfo = getCurrentModelInfo();
  143. button.textContent = initialModelInfo.displayName;
  144.  
  145. modelDisplayNames.forEach((name, index) => {
  146. const modelId = modelIds[index];
  147. const item = document.createElement('a');
  148. item.className = 'model-switcher-item';
  149. item.textContent = name;
  150. item.dataset.modelId = modelId;
  151. item.href = '#';
  152.  
  153. if (initialModelInfo.currentId === modelId) {
  154. item.classList.add('active');
  155. }
  156.  
  157. item.addEventListener('click', (e) => {
  158. e.preventDefault();
  159. e.stopPropagation();
  160.  
  161. const selectedModelId = e.target.dataset.modelId;
  162. const currentModelIdNow = getCurrentModelInfo().currentId;
  163.  
  164. if (currentModelIdNow !== selectedModelId) {
  165. const url = new URL(window.location.href);
  166. url.searchParams.set('model', selectedModelId);
  167. console.log(`${SCRIPT_PREFIX} 切换目标: ${name} (${selectedModelId})`);
  168. window.location.href = url.toString();
  169. // hideDropdown();
  170. } else {
  171. console.log(`${SCRIPT_PREFIX} 模型无需切换`);
  172. hideDropdown();
  173. }
  174. });
  175. dropdown.appendChild(item);
  176. });
  177.  
  178. button.addEventListener('click', (e) => {
  179. e.stopPropagation();
  180. toggleDropdown();
  181. });
  182.  
  183. container.appendChild(button);
  184. dropdownElement = dropdown;
  185. return container;
  186. }
  187.  
  188. function showDropdown() {
  189. if (!dropdownElement || isDropdownVisible) return;
  190.  
  191. const button = document.getElementById('model-switcher-button');
  192. if (!button) {
  193. console.error(`${SCRIPT_PREFIX} 找不到添加的模型切换器按钮,无法定位下拉菜单。`);
  194. return;
  195. }
  196.  
  197. if (!dropdownElement.parentElement) {
  198. document.body.appendChild(dropdownElement);
  199. }
  200. isDropdownVisible = true;
  201.  
  202. const buttonRect = button.getBoundingClientRect();
  203. const dropdownHeight = dropdownElement.offsetHeight;
  204. const dropdownWidth = dropdownElement.offsetWidth;
  205. const spaceAbove = buttonRect.top;
  206. const spaceBelow = window.innerHeight - buttonRect.bottom;
  207. const margin = 5;
  208. let top, left = buttonRect.left;
  209. if (spaceBelow >= dropdownHeight + margin || spaceBelow >= spaceAbove) {
  210. top = buttonRect.bottom + margin;
  211. } else {
  212. top = buttonRect.top - dropdownHeight - margin;
  213. }
  214. top = Math.max(margin, Math.min(top, window.innerHeight - dropdownHeight - margin));
  215. left = Math.max(margin, Math.min(left, window.innerWidth - dropdownWidth - margin));
  216. dropdownElement.style.top = `${top}px`;
  217. dropdownElement.style.left = `${left}px`;
  218.  
  219. document.addEventListener('click', handleClickOutside, true);
  220. window.addEventListener('resize', debouncedHideDropdown);
  221. window.addEventListener('scroll', debouncedHideDropdownOnScroll, true);
  222. }
  223.  
  224. function hideDropdown() {
  225. if (!dropdownElement || !isDropdownVisible) return;
  226. if (dropdownElement.parentElement) {
  227. dropdownElement.remove();
  228. }
  229. isDropdownVisible = false;
  230. document.removeEventListener('click', handleClickOutside, true);
  231. window.removeEventListener('resize', debouncedHideDropdown);
  232. window.removeEventListener('scroll', debouncedHideDropdownOnScroll, true);
  233. }
  234. const debouncedHideDropdown = debounce(hideDropdown, 150);
  235. const debouncedHideDropdownOnScroll = debounce(hideDropdown, 150);
  236.  
  237. function toggleDropdown() {
  238. if (isDropdownVisible) {
  239. hideDropdown();
  240. } else {
  241. showDropdown();
  242. }
  243. }
  244.  
  245. function handleClickOutside(event) {
  246. const button = document.getElementById('model-switcher-button');
  247. if (dropdownElement && dropdownElement.parentNode && button &&
  248. !button.contains(event.target) && !dropdownElement.contains(event.target)) {
  249. hideDropdown();
  250. }
  251. }
  252.  
  253. function updateSwitcherState() {
  254. const button = document.getElementById('model-switcher-button');
  255. if (!button) return;
  256.  
  257. const currentInfo = getCurrentModelInfo();
  258.  
  259. if (button.textContent !== currentInfo.displayName) {
  260. button.textContent = currentInfo.displayName;
  261. console.log(`${SCRIPT_PREFIX} 按钮文本更新为: ${currentInfo.displayName}`);
  262. }
  263.  
  264. if (dropdownElement) {
  265. const items = dropdownElement.querySelectorAll('.model-switcher-item');
  266. let changed = false;
  267. items.forEach((item) => {
  268. const modelId = item.dataset.modelId;
  269. const shouldBeActive = (currentInfo.currentId === modelId);
  270. if (item.classList.contains('active') !== shouldBeActive) {
  271. if (shouldBeActive) {
  272. item.classList.add('active');
  273. } else {
  274. item.classList.remove('active');
  275. }
  276. changed = true;
  277. }
  278. });
  279. if (changed) {
  280. console.log(`${SCRIPT_PREFIX} 下拉菜单激活状态已更新`);
  281. }
  282. }
  283. }
  284.  
  285. function checkAndInsertSwitcher() {
  286. const composerSelector = 'form[data-type="unified-composer"]';
  287. const toolbarContainer = document.querySelector(composerSelector);
  288.  
  289. if (!toolbarContainer) {
  290. const existingSwitcher = document.getElementById('model-switcher-container');
  291. if (existingSwitcher && !existingSwitcher.closest(composerSelector)) {
  292. console.log(`${SCRIPT_PREFIX} 切换器存在于预期容器之外,移除。`);
  293. existingSwitcher.remove();
  294. insertionAttempted = false;
  295. hideDropdown();
  296. }
  297. return;
  298. }
  299.  
  300. const existingSwitcher = document.getElementById('model-switcher-container');
  301. const targetSelector = 'div[style*="--vt-composer-attach-file-action"]';
  302. const insertionTarget = toolbarContainer.querySelector(targetSelector);
  303.  
  304. if (existingSwitcher) {
  305. if (!toolbarContainer.contains(existingSwitcher)) {
  306. console.log(`${SCRIPT_PREFIX} 切换器存在但不在预期容器内,移动它...`);
  307. if (insertionTarget) {
  308. insertionTarget.insertAdjacentElement('afterend', existingSwitcher);
  309. } else {
  310. toolbarContainer.appendChild(existingSwitcher);
  311. console.log(`${SCRIPT_PREFIX} 未找到附件按钮,切换器移动到工具栏末尾`);
  312. }
  313. }
  314. updateSwitcherState();
  315. insertionAttempted = true;
  316. } else {
  317. if (insertionTarget) {
  318. console.log(`${SCRIPT_PREFIX} 找到插入点 (${targetSelector}),尝试插入切换器...`);
  319. const switcherContainer = createModelSwitcher();
  320. insertionTarget.insertAdjacentElement('afterend', switcherContainer);
  321. console.log(`${SCRIPT_PREFIX} 成功插入至附件上传按钮之后`);
  322. insertionAttempted = true;
  323. } else {
  324. console.warn(`${SCRIPT_PREFIX} 在工具栏内未找到附件上传按钮 (${targetSelector}),尝试添加到工具栏末尾`);
  325. const switcherContainer = createModelSwitcher();
  326. toolbarContainer.appendChild(switcherContainer);
  327. console.log(`${SCRIPT_PREFIX} 成功插入至工具栏末尾`);
  328. insertionAttempted = true;
  329. }
  330. }
  331.  
  332. const currentSwitcherExists = !!document.getElementById('model-switcher-container');
  333. if (insertionAttempted && !currentSwitcherExists) {
  334. console.log(`${SCRIPT_PREFIX} 切换器容器似乎已被移除, 将在下次检查时尝试重新插入`);
  335. insertionAttempted = false;
  336. hideDropdown();
  337. }
  338. }
  339.  
  340. const debouncedCheckAndInsert = debounce(checkAndInsertSwitcher, DEBOUNCE_DELAY);
  341.  
  342. const observer = new MutationObserver((mutationsList, obs) => {
  343. debouncedCheckAndInsert();
  344. });
  345.  
  346. console.log(`${SCRIPT_PREFIX} 正在监控 DOM 变化`);
  347. observer.observe(document.body, {
  348. childList: true,
  349. subtree: true
  350. });
  351.  
  352. console.log(`${SCRIPT_PREFIX} 计划在 500ms 后进行首次检查`);
  353. setTimeout(debouncedCheckAndInsert, 500);
  354.  
  355. })();