Spaces:
Paused
Paused
File size: 16,728 Bytes
ac029f2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
// ==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.');
})(); |