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