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.');

})();