hins111's picture
Upload 7 files
ac029f2 verified
// ==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.');
})();