Switch Bug Team Model

Bug Team,好用、爱用 ♥

当前为 2025-04-20 提交的版本,查看 最新版本

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