Spaces:
Paused
Paused
| // ==UserScript== | |
| // @name Google AI Studio Model Injector (Multi-Model) - XHR+Fetch+ArrayStructure | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.6 | |
| // @description Inject multiple custom models with themed emojis (Kingfall, Gemini, Goldmane, etc.) into the model list on Google AI Studio. Intercepts XHR/fetch, handles array-of-arrays JSON structure. | |
| // @author Generated by AI / HCPTangHY / Mozi | |
| // @match https://aistudio.google.com/* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=aistudio.google.com | |
| // @grant none | |
| // @run-at document-start | |
| // @license MIT | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // ================== 定义要注入的模型列表 ===================== | |
| // ↓↓↓↓↓↓ 版本号 / EMOJIS 更新 ↓↓↓↓↓↓ | |
| const SCRIPT_VERSION = "v1.6"; | |
| // ↓↓↓↓↓↓ 模型列表 EMOJIS 已更新 ↓↓↓↓↓↓ | |
| const MODELS_TO_INJECT = [ | |
| { | |
| name: 'models/kingfall-ab-test', | |
| displayName: `👑 Kingfall (Script ${SCRIPT_VERSION})`, // 👑 King | |
| description: `Model injected by script ${SCRIPT_VERSION}` | |
| }, | |
| { | |
| name: 'models/gemini-2.5-pro-preview-03-25', | |
| displayName: `✨ Gemini 2.5 Pro 03-25 (Script ${SCRIPT_VERSION})`, // ✨ Magic/AI | |
| description: `Model injected by script ${SCRIPT_VERSION}` | |
| }, | |
| { | |
| name: 'models/goldmane-ab-test', | |
| displayName: `🦁 Goldmane (Script ${SCRIPT_VERSION})`, // 🦁 Gold Mane | |
| description: `Model injected by script ${SCRIPT_VERSION}` | |
| }, | |
| { | |
| name: 'models/claybrook-ab-test', | |
| displayName: `💧 Claybrook (Script ${SCRIPT_VERSION})`, // 💧 Brook | |
| description: `Model injected by script ${SCRIPT_VERSION}` | |
| }, | |
| { | |
| name: 'models/frostwind-ab-test', | |
| displayName: `❄️ Frostwind (Script ${SCRIPT_VERSION})`, // ❄️ Frost | |
| description: `Model injected by script ${SCRIPT_VERSION}` | |
| }, | |
| { | |
| name: 'models/calmriver-ab-test', | |
| displayName: `🌊 Calmriver (Script ${SCRIPT_VERSION})`, // 🌊 River | |
| description: `Model injected by script ${SCRIPT_VERSION}` | |
| }, | |
| // 可以在此按格式继续添加更多模型 | |
| ]; | |
| // ↑↑↑↑↑↑ 模型列表 EMOJIS 已更新 ↑↑↑↑↑↑ | |
| // ========================================================== | |
| const LOG_PREFIX = `[AI Studio Injector ${SCRIPT_VERSION}]:`; | |
| const ANTI_HIJACK_PREFIX = ")]}'\n"; | |
| // --- 关键索引定义 (基于您的JSON结构) --- | |
| const NAME_IDX = 0; | |
| const DISPLAY_NAME_IDX = 3; | |
| const DESC_IDX = 4; | |
| const METHODS_IDX = 7; | |
| // ------------------------------------ | |
| console.log(LOG_PREFIX, 'Script active. Patching Fetch and XHR...'); | |
| function isTargetURL(url) { | |
| return url && typeof url === 'string' && url.includes('alkalimakersuite') && url.includes('/ListModels'); | |
| } | |
| // 递归查找包含模型数组的数组: 寻找 `[ [model1_array], [model2_array], ... ]` | |
| function findModelListArray(obj) { | |
| if (!obj) return null; | |
| // 检查 obj 是否是我们寻找的那个包含多个模型数组的数组 | |
| if (Array.isArray(obj) && obj.length > 0 && obj.every( | |
| item => Array.isArray(item) && typeof item[NAME_IDX] === 'string' && String(item[NAME_IDX]).startsWith('models/') | |
| )) { | |
| // console.log(LOG_PREFIX, "Target model list array FOUND."); // Reduce log noise | |
| return obj; | |
| } | |
| // 递归搜索 | |
| if (typeof obj === 'object') { | |
| for (const key in obj) { | |
| if (Object.prototype.hasOwnProperty.call(obj, key)) { | |
| if(typeof obj[key] === 'object' && obj[key] !== null){ // check not null | |
| const result = findModelListArray(obj[key]); | |
| if (result) return result; | |
| } | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| // 核心:处理并修改 JSON 数据 - 使用索引访问,遍历多个模型 | |
| function processJsonData(jsonData, url) { | |
| let modificationMade = false; | |
| const modelsArray = findModelListArray(jsonData); // [ [模型1数组], [模型2数组], ...] | |
| if(modelsArray && Array.isArray(modelsArray)){ | |
| // console.log(LOG_PREFIX, 'Processing models array (length:', modelsArray.length, ") for URL:", url); // Reduce log noise | |
| // *** 只寻找一次模板 *** | |
| const templateModel = modelsArray.find(m => Array.isArray(m) && m[NAME_IDX] && String(m[NAME_IDX]).includes('flash') && Array.isArray(m[METHODS_IDX]) ) | |
| || modelsArray.find(m => Array.isArray(m) && m[NAME_IDX] && String(m[NAME_IDX]).includes('pro') && Array.isArray(m[METHODS_IDX]) ) | |
| || modelsArray.find(m => Array.isArray(m) && m[NAME_IDX] && Array.isArray(m[METHODS_IDX]) ); // 找第一个有方法的 | |
| const templateName = (templateModel && templateModel[NAME_IDX]) ? templateModel[NAME_IDX] : 'unknown'; | |
| if(templateModel){ | |
| // console.log(LOG_PREFIX, `Using template: ${templateName}`); // Reduce log noise | |
| } else { | |
| console.warn(LOG_PREFIX, 'Could not find a suitable template model array. Cannot inject new models, but can update existing ones.'); | |
| } | |
| // *** 遍历所有需要注入的模型 *** | |
| // 使用 reverse, 使得 MODELS_TO_INJECT 数组中的顺序和最终显示在顶部的顺序一致 | |
| [...MODELS_TO_INJECT].reverse().forEach(modelToInject => { | |
| const modelExists = modelsArray.some(model => Array.isArray(model) && model[NAME_IDX] === modelToInject.name); | |
| if (!modelExists) { | |
| if(!templateModel) { | |
| console.warn(LOG_PREFIX, `Cannot inject ${modelToInject.name}: No template found.`); | |
| return; // Skip this model if no template | |
| } | |
| // !!!关键: 必须深拷贝数组!!! | |
| const newModel = JSON.parse(JSON.stringify(templateModel)); // Deep Clone from template | |
| // !!!关键: 使用索引修改 !!! | |
| newModel[NAME_IDX] = modelToInject.name; | |
| newModel[DISPLAY_NAME_IDX] = modelToInject.displayName; | |
| newModel[DESC_IDX] = `${modelToInject.description} (Structure based on ${templateName})`; | |
| if(!Array.isArray(newModel[METHODS_IDX])){ | |
| newModel[METHODS_IDX] = ["generateContent", "countTokens","createCachedContent","batchGenerateContent"]; | |
| } | |
| modelsArray.unshift(newModel); // 添加到开头 | |
| modificationMade = true; | |
| console.log(LOG_PREFIX, `Successfully INJECTED: ${modelToInject.displayName}`); | |
| } else { | |
| // console.log(LOG_PREFIX, `Model ALREADY EXISTS: ${modelToInject.name}. Checking displayName.`); // Reduce log noise | |
| const existing = modelsArray.find(model => Array.isArray(model) && model[NAME_IDX] === modelToInject.name); | |
| // 如果存在,但名字不是脚本设定的名字,且不包含当前版本号,则更新名字 | |
| // 如果模型已存在,且名字就是我们设定的(例如刷新页面),我们也更新一下,确保emoji和版本号是最新的 | |
| if(existing && existing[DISPLAY_NAME_IDX] !== modelToInject.displayName) { | |
| // 检查是否只是版本号或emoji不同 | |
| const baseExistingName = String(existing[DISPLAY_NAME_IDX]).replace(/ \(Script v\d+\.\d+(-beta\d*)?\)/, '').replace(/^[👑✨🦁💧❄️🌊]\s*/,'').trim(); | |
| const baseInjectName = modelToInject.displayName.replace(/ \(Script v\d+\.\d+(-beta\d*)?\)/, '').replace(/^[👑✨🦁💧❄️🌊]\s*/,'').trim(); | |
| if (baseExistingName === baseInjectName) { | |
| // 基础名字相同,只更新emoji和版本号 | |
| existing[DISPLAY_NAME_IDX] = modelToInject.displayName; | |
| console.log(LOG_PREFIX, `Updated Emoji/Version for ${modelToInject.displayName}.`); | |
| } else { | |
| // 基础名字不同,说明是官方自带的或其他来源,加上 (Orig) | |
| existing[DISPLAY_NAME_IDX] = modelToInject.displayName + " (Orig)"; | |
| console.log(LOG_PREFIX, `Updated displayName for existing official ${modelToInject.name} to (Orig).`); | |
| } | |
| modificationMade = true; | |
| } | |
| } | |
| }); // End forEach | |
| } else { | |
| console.warn(LOG_PREFIX, 'URL matched, but no valid model list array structure found in JSON for:', url); | |
| } | |
| return { data: jsonData, modified: modificationMade }; | |
| } | |
| // 统一处理响应体文本 (解析, 处理, 序列化) | |
| function modifyResponseBody(originalText, url) { | |
| if (!originalText || typeof originalText !== 'string') return originalText; // Add type check | |
| try { | |
| let textBody = originalText; | |
| let hasPrefix = false; | |
| if (textBody.startsWith(ANTI_HIJACK_PREFIX)) { | |
| textBody = textBody.substring(ANTI_HIJACK_PREFIX.length); | |
| hasPrefix = true; | |
| } | |
| if(!textBody.trim()) return originalText; | |
| const jsonData = JSON.parse(textBody); | |
| const result = processJsonData(jsonData, url); | |
| if (result.modified) { | |
| let newBody = JSON.stringify(result.data); | |
| if(hasPrefix){ | |
| newBody = ANTI_HIJACK_PREFIX + newBody; | |
| } | |
| // console.log(LOG_PREFIX, "Returning MODIFIED response body."); // Reduce log noise | |
| return newBody; | |
| } | |
| } catch (error) { | |
| console.error(LOG_PREFIX, 'Error processing response body for:', url, error, "\nOriginal Text snippet:", String(originalText).substring(0, 300) + "..."); | |
| } | |
| // console.log(LOG_PREFIX, "Returning ORIGINAL response body (no modification or error)."); // Reduce log noise | |
| return originalText; | |
| } | |
| //================================== | |
| // 拦截 Fetch (保留) | |
| //================================== | |
| const originalFetch = window.fetch; | |
| window.fetch = async function(...args) { | |
| const resource = args[0]; | |
| const url = (resource instanceof Request) ? resource.url : String(resource); | |
| const response = await originalFetch.apply(this, args); | |
| if (isTargetURL(url) && response.ok) { | |
| console.log(LOG_PREFIX, '[Fetch] Intercepting:', url); | |
| try { | |
| const cloneResponse = response.clone(); | |
| const originalText = await cloneResponse.text(); | |
| const newBody = modifyResponseBody(originalText, url); | |
| if(newBody !== originalText){ | |
| return new Response(newBody, { status: response.status, statusText: response.statusText, headers: response.headers }); | |
| } | |
| } catch(e) { console.error(LOG_PREFIX, '[Fetch] Error:', e); } | |
| } | |
| return response; | |
| }; | |
| console.log(LOG_PREFIX, 'Fetch patch applied.'); | |
| //================================== | |
| // 拦截 XMLHttpRequest (XHR) | |
| //================================== | |
| const xhrProto = XMLHttpRequest.prototype; | |
| const originalOpen = xhrProto.open; | |
| const originalResponseTextDescriptor = Object.getOwnPropertyDescriptor(xhrProto, 'responseText'); | |
| const originalResponseDescriptor = Object.getOwnPropertyDescriptor(xhrProto, 'response'); | |
| let interceptionCount = 0; | |
| xhrProto.open = function(method, url) { | |
| this._interceptorUrl = url; | |
| this._isTargetXHR = isTargetURL(url); | |
| if(this._isTargetXHR){ | |
| interceptionCount++; | |
| console.log(LOG_PREFIX, `[XHR] Open detected (${interceptionCount}) for:`, url); | |
| } | |
| return originalOpen.apply(this, arguments); | |
| }; | |
| const handleXHRResponse = (xhr, originalValue, type = 'text') => { | |
| if (xhr._isTargetXHR && xhr.readyState === 4 && xhr.status === 200) { | |
| const cacheKey = '_modifiedResponseCache_' + type; | |
| if(xhr[cacheKey] === undefined){ | |
| // console.log(LOG_PREFIX, `[XHR] Processing response[${type}] for:`, xhr._interceptorUrl); // Reduce log noise | |
| const originalText = (type === 'text' || typeof originalValue !== 'object' || originalValue === null) ? String(originalValue || '') : JSON.stringify(originalValue) ; | |
| // Cache the result of modifyResponseBody | |
| xhr[cacheKey] = modifyResponseBody(originalText, xhr._interceptorUrl); | |
| } | |
| // Use the cached result | |
| const cachedResponse = xhr[cacheKey]; | |
| try{ | |
| // 如果是对象类型,且缓存的是字符串,返回时需要反序列化 | |
| if (type === 'json' && typeof cachedResponse === 'string') { | |
| // Ensure the string is not empty before parsing | |
| const textToParse = cachedResponse.replace(ANTI_HIJACK_PREFIX,''); | |
| if (textToParse) { | |
| return JSON.parse(textToParse); | |
| } | |
| return null; // or originalValue if parsing empty string is an issue | |
| } | |
| } catch(e){ | |
| console.error(LOG_PREFIX, `[XHR] Error parsing cached JSON for type 'json': `, e, `Cache content: ${String(cachedResponse).substring(0,100)}...`); | |
| return originalValue; // fallback | |
| } | |
| return cachedResponse; // text type or already object or empty string | |
| } | |
| return originalValue; | |
| }; | |
| if(originalResponseTextDescriptor && originalResponseTextDescriptor.get) { | |
| Object.defineProperty(xhrProto, 'responseText', { | |
| get: function() { | |
| const originalText = originalResponseTextDescriptor.get.call(this); | |
| // Only handle if responseType is text or default "" | |
| if (this.responseType && this.responseType !== 'text' && this.responseType !== "") return originalText; | |
| return handleXHRResponse(this, originalText, 'text'); | |
| }, configurable: true | |
| }); | |
| console.log(LOG_PREFIX, 'XHR responseText patch applied.'); | |
| } else { console.error(LOG_PREFIX, 'XHR: Failed to get original responseText descriptor!'); } | |
| if(originalResponseDescriptor && originalResponseDescriptor.get) { | |
| Object.defineProperty(xhrProto, 'response', { | |
| get: function() { | |
| const originalResponse = originalResponseDescriptor.get.call(this); | |
| if (this.responseType === 'json') { | |
| return handleXHRResponse(this, originalResponse, 'json'); | |
| } | |
| // When responseType is "" or "text", originalResponse is the text itself | |
| if (!this.responseType || this.responseType === 'text' || this.responseType === "") { | |
| return handleXHRResponse(this, originalResponse, 'text'); | |
| } | |
| return originalResponse; // other types like blob, arraybuffer | |
| }, configurable: true | |
| }); | |
| console.log(LOG_PREFIX, 'XHR response patch applied.'); | |
| } else { | |
| console.error(LOG_PREFIX, 'XHR: Failed to get original response descriptor!'); | |
| } | |
| console.log(LOG_PREFIX, 'XHR open patch applied.'); | |
| })(); |