Google AI Studio 模型注入器(多模型版)- 修复Token计算

注入自定义模型到 Google AI Studio。拦截 XHR/Fetch 请求处理模型列表,并拦截 CountTokens 请求以使用最新的 Gemini 2.5 Pro 06-05 模型进行计算。

// ==UserScript==
// @name         Google AI Studio 模型注入器(多模型版)- 修复Token计算
// @namespace    http://tampermonkey.net/
// @version      1.6.8
// @description  注入自定义模型到 Google AI Studio。拦截 XHR/Fetch 请求处理模型列表,并拦截 CountTokens 请求以使用最新的 Gemini 2.5 Pro 06-05 模型进行计算。
// @author       Generated by AI / HCPTangHY / Mozi / wisdgod / UserModified / Z_06 (Token Fix) / AI (Emoji Update)
// @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';

    // ==================== 配置区域 ====================
    const SCRIPT_VERSION = "v1.6.8";
    const LOG_PREFIX = `[AI Studio 注入器 ${SCRIPT_VERSION}]`;
    const ANTI_HIJACK_PREFIX = ")]}'\n";

    // --- Token 计算备用模型配置 ---
    // 已更新为最新的 Gemini 2.5 Pro 06-05 版本
    const TOKEN_COUNT_FALLBACK_MODEL = 'models/gemini-2.5-pro-preview-06-05';

    // 模型配置列表
    const MODELS_TO_INJECT = [
        // --- 乱码模型组 (按性能排序) ---
        {
            name: 'models/jfdksal98a',
            displayName: `💎 jfdksal98a (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/68zkqbz8vs',
            displayName: `🔮 68zkqbz8vs (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/a24bo28u1a',
            displayName: `⚡ a24bo28u1a (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/2vmc1bo4ri',
            displayName: `🚀 2vmc1bo4ri (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/42fc3y4xfsz',
            displayName: `🤖 42fc3y4xfsz (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/ixqzem8yj4j',
            displayName: `⚙️ ixqzem8yj4j (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/oiy9yghoam',
            displayName: `🔧 oiy9yghoam (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },

        // --- 其他已知模型 ---
        {
            name: 'models/blacktooth-ab-test',
            displayName: `🏴‍☠️ Blacktooth (AB-Test) (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/gemini-2.5-pro-preview-03-25',
            displayName: `✨ Gemini 2.5 Pro 03-25 (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/goldmane-ab-test',
            displayName: `🦁 Goldmane (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/claybrook-ab-test',
            displayName: `💧 Claybrook (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/frostwind-ab-test',
            displayName: `❄️ Frostwind (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        },
        {
            name: 'models/calmriver-ab-test',
            displayName: `🌊 Calmriver (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 注入的模型`
        }
    ];

    // JSON 结构中的字段索引
    const MODEL_FIELDS = {
        NAME: 0,
        DISPLAY_NAME: 3,
        DESCRIPTION: 4,
        METHODS: 7
    };

    // ==================== 工具函数 ====================

    /**
     * 检查 URL 是否为 ListModels API 端点
     * @param {string} url - 要检查的 URL
     * @returns {boolean}
     */
    function isListModelsURL(url) {
        return url && typeof url === 'string' &&
               url.includes('alkalimakersuite') &&
               url.includes('/ListModels');
    }

    /**
     * 检查 URL 是否为 CountTokens API 端点
     * @param {string} url - 要检查的 URL
     * @returns {boolean}
     */
    function isCountTokensURL(url) {
        return url && typeof url === 'string' &&
               url.includes('alkalimakersuite') &&
               url.includes('/CountTokens');
    }

    /**
     * 递归查找模型列表数组
     * @param {any} obj - 要搜索的对象
     * @returns {Array|null} 找到的模型数组或 null
     */
    function findModelListArray(obj) {
        if (!obj) return null;

        // 检查是否为目标模型数组
        if (Array.isArray(obj) && obj.length > 0 && obj.every(
            item => Array.isArray(item) &&
                    typeof item[MODEL_FIELDS.NAME] === 'string' &&
                    String(item[MODEL_FIELDS.NAME]).startsWith('models/')
        )) {
            return obj;
        }

        // 递归搜索子对象
        if (typeof obj === 'object') {
            for (const key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key) &&
                    typeof obj[key] === 'object' &&
                    obj[key] !== null) {
                    const result = findModelListArray(obj[key]);
                    if (result) return result;
                }
            }
        }
        return null;
    }

    /**
     * 查找合适的模板模型
     * @param {Array} modelsArray - 模型数组
     * @returns {Array|null} 模板模型或 null
     */
    function findTemplateModel(modelsArray) {
        // 优先查找包含特定关键词的模型
        const templateModel =
            modelsArray.find(m => Array.isArray(m) &&
                                 m[MODEL_FIELDS.NAME] &&
                                 String(m[MODEL_FIELDS.NAME]).includes('pro') &&
                                 Array.isArray(m[MODEL_FIELDS.METHODS])) ||
            modelsArray.find(m => Array.isArray(m) &&
                                 m[MODEL_FIELDS.NAME] &&
                                 String(m[MODEL_FIELDS.NAME]).includes('flash') &&
                                 Array.isArray(m[MODEL_FIELDS.METHODS])) ||
            modelsArray.find(m => Array.isArray(m) &&
                                 m[MODEL_FIELDS.NAME] &&
                                 Array.isArray(m[MODEL_FIELDS.METHODS]));

        return templateModel;
    }

    /**
     * 更新已存在模型的显示名称
     * @param {Array} existingModel - 现有模型
     * @param {Object} modelToInject - 要注入的模型配置
     * @returns {boolean} 是否进行了更新
     */
    function updateExistingModel(existingModel, modelToInject) {
        if (!existingModel || existingModel[MODEL_FIELDS.DISPLAY_NAME] === modelToInject.displayName) {
            return false;
        }

        // 提取基础名称(去除版本号和表情)
        const cleanName = (name) => String(name)
            .replace(/ \(脚本 v\d+\.\d+(\.\d+)?(-beta\d*)?\)/, '')
            // 包含所有当前使用的表情,包括新增的 🔮💎⚡🚀🤖⚙️🔧
            .replace(/^[✨🦁💧❄️🌊🐉🏴‍☠️🔮💎⚡🚀🤖⚙️🔧]\s*/, '')
            .trim();

        const baseExistingName = cleanName(existingModel[MODEL_FIELDS.DISPLAY_NAME]);
        const baseInjectName = cleanName(modelToInject.displayName);

        if (baseExistingName === baseInjectName) {
            // 仅更新版本号和表情
            existingModel[MODEL_FIELDS.DISPLAY_NAME] = modelToInject.displayName;
            console.log(LOG_PREFIX, `已更新表情/版本号: ${modelToInject.displayName}`);
            return true;
        }

        // 如果是官方模型和注入模型名称不一致但模型ID相同的情况,可以考虑保留官方名称或标记
        // 但对于乱码模型,通常它们不会出现在官方列表中,所以这部分逻辑可以简化或忽略
        return false;
    }

    /**
     * 创建新模型
     * @param {Array} templateModel - 模板模型
     * @param {Object} modelToInject - 要注入的模型配置
     * @param {string} templateName - 模板名称
     * @returns {Array} 新模型数组
     */
    function createNewModel(templateModel, modelToInject, templateName) {
        const newModel = structuredClone(templateModel);

        newModel[MODEL_FIELDS.NAME] = modelToInject.name;
        newModel[MODEL_FIELDS.DISPLAY_NAME] = modelToInject.displayName;
        newModel[MODEL_FIELDS.DESCRIPTION] = `${modelToInject.description} (基于 ${templateName} 结构)`;

        // 确保 methods 存在,这让 UI 认为它可以调用 countTokens
        if (!Array.isArray(newModel[MODEL_FIELDS.METHODS])) {
            newModel[MODEL_FIELDS.METHODS] = [
                "generateContent",
                "countTokens", // 即使后端不支持,我们也在这里声明支持,以便后续拦截
                "createCachedContent",
                "batchGenerateContent"
            ];
        } else if (!newModel[MODEL_FIELDS.METHODS].includes("countTokens")) {
             newModel[MODEL_FIELDS.METHODS].push("countTokens");
        }

        return newModel;
    }

    // ==================== 响应处理函数 (用于 ListModels) ====================

    /**
     * 处理并修改 ListModels 返回的 JSON 数据
     * @param {Object} jsonData - 原始 JSON 数据
     * @param {string} url - 请求 URL
     * @returns {Object} 包含处理后数据和修改标志的对象
     */
    function processListModelsResponse(jsonData, url) {
        let modificationMade = false;
        const modelsArray = findModelListArray(jsonData);

        if (!modelsArray || !Array.isArray(modelsArray)) {
            console.warn(LOG_PREFIX, '在 JSON 中未找到有效的模型列表结构:', url);
            return { data: jsonData, modified: false };
        }

        // 查找模板模型
        const templateModel = findTemplateModel(modelsArray);
        const templateName = templateModel?.[MODEL_FIELDS.NAME] || 'unknown';

        if (!templateModel) {
            console.warn(LOG_PREFIX, '未找到合适的模板模型,无法注入新模型');
        }

        // 反向遍历以保持显示顺序 (配置中靠前的模型显示在最上面)
        [...MODELS_TO_INJECT].reverse().forEach(modelToInject => {
            const existingModel = modelsArray.find(
                model => Array.isArray(model) && model[MODEL_FIELDS.NAME] === modelToInject.name
            );

            if (!existingModel) {
                // 注入新模型
                if (!templateModel) {
                    console.warn(LOG_PREFIX, `无法注入 ${modelToInject.name}:缺少模板`);
                    return;
                }

                const newModel = createNewModel(templateModel, modelToInject, templateName);
                modelsArray.unshift(newModel); // unshift 将模型添加到数组开头
                modificationMade = true;
                console.log(LOG_PREFIX, `成功注入: ${modelToInject.displayName}`);
            } else {
                // 更新现有模型
                if (updateExistingModel(existingModel, modelToInject)) {
                    modificationMade = true;
                }
            }
        });

        return { data: jsonData, modified: modificationMade };
    }

    /**
     * 修改 ListModels 响应体
     * @param {string} originalText - 原始响应文本
     * @param {string} url - 请求 URL
     * @returns {string} 修改后的响应文本
     */
    function modifyListModelsResponseBody(originalText, url) {
        if (!originalText || typeof originalText !== 'string') {
            return originalText;
        }

        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 = processListModelsResponse(jsonData, url);

            if (result.modified) {
                let newBody = JSON.stringify(result.data);
                if (hasPrefix) {
                    newBody = ANTI_HIJACK_PREFIX + newBody;
                }
                return newBody;
            }
        } catch (error) {
            console.error(LOG_PREFIX, '处理 ListModels 响应体时出错:', url, error);
        }

        return originalText;
    }

    // ==================== 请求处理函数 (用于 CountTokens) ====================

    /**
     * 修改 CountTokens 请求体,替换模型名称
     * @param {string} originalBody - 原始请求体 (JSON 字符串)
     * @returns {Object} 包含修改后请求体和修改标志的对象
     */
    function modifyCountTokensRequestBody(originalBody) {
        try {
            const payload = JSON.parse(originalBody);
            // AI Studio 的请求负载结构通常是一个数组: [ "models/the-model-name", { ...content... } ]
            if (Array.isArray(payload) && typeof payload[0] === 'string' && payload[0].startsWith('models/')) {
                const requestedModel = payload[0];

                // 检查请求的模型是否是我们注入的隐藏模型之一
                const isHiddenModel = MODELS_TO_INJECT.some(m => m.name === requestedModel);

                if (isHiddenModel && requestedModel !== TOKEN_COUNT_FALLBACK_MODEL) {
                    console.log(LOG_PREFIX, `[Token Count 拦截] 将请求模型从 ${requestedModel} 重定向到 ${TOKEN_COUNT_FALLBACK_MODEL}`);
                    payload[0] = TOKEN_COUNT_FALLBACK_MODEL; // 替换为备用模型
                    return { body: JSON.stringify(payload), modified: true };
                }
            }
        } catch (e) {
            console.error(LOG_PREFIX, "解析或修改 CountTokens 请求体时出错:", e);
        }
        return { body: originalBody, modified: false };
    }

    // ==================== 请求/响应拦截 ====================

    // 拦截 Fetch API
    const originalFetch = window.fetch;
    window.fetch = async function(...args) {
        const resource = args[0];
        const url = (resource instanceof Request) ? resource.url : String(resource);

        // --- 拦截 1: 出站请求 (CountTokens) ---
        // AI Studio 主要使用 XHR,Fetch 拦截作为备用

        // 执行原始 Fetch
        const response = await originalFetch.apply(this, args);

        // --- 拦截 2: 入站响应 (ListModels) ---
        if (isListModelsURL(url) && response.ok) {
            console.log(LOG_PREFIX, '[Fetch] 拦截到 ListModels 响应:', url);
            try {
                const cloneResponse = response.clone();
                const originalText = await cloneResponse.text();
                const newBody = modifyListModelsResponseBody(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] 处理 ListModels 响应错误:', e);
            }
        }
        return response;
    };

    // 拦截 XMLHttpRequest
    const xhrProto = XMLHttpRequest.prototype;
    const originalOpen = xhrProto.open;
    const originalSend = xhrProto.send; // 保存原始 send 方法
    const originalResponseTextDescriptor = Object.getOwnPropertyDescriptor(xhrProto, 'responseText');
    const originalResponseDescriptor = Object.getOwnPropertyDescriptor(xhrProto, 'response');
    let listModelsInterceptionCount = 0;

    // 重写 open 方法 (记录请求类型)
    xhrProto.open = function(method, url) {
        this._interceptorUrl = url;
        this._isListModelsXHR = isListModelsURL(url); // 标记是否为 ListModels 请求
        this._isCountTokensXHR = isCountTokensURL(url); // 标记是否为 CountTokens 请求

        if (this._isListModelsXHR) {
            listModelsInterceptionCount++;
            console.log(LOG_PREFIX, `[XHR Open] 检测到 ListModels 请求 (${listModelsInterceptionCount}):`, url);
        }

        return originalOpen.apply(this, arguments);
    };

    // 重写 send 方法 (修改 CountTokens 请求)
    xhrProto.send = function(data) {
        // 检查是否为 CountTokens 请求,并且有数据体
        if (this._isCountTokensXHR && data && typeof data === 'string') {
            const { body: modifiedData, modified } = modifyCountTokensRequestBody(data);
            if (modified) {
                 // Log已经在modifyCountTokensRequestBody中打印
            }
            return originalSend.call(this, modifiedData);
        }
        // 对于其他请求,正常发送
        return originalSend.call(this, data);
    };

    /**
     * 处理 XHR 响应 (修改 ListModels 响应)
     * @param {XMLHttpRequest} xhr - XHR 对象
     * @param {any} originalValue - 原始响应值
     * @param {string} type - 响应类型
     * @returns {any} 处理后的响应值
     */
    const handleXHRResponse = (xhr, originalValue, type = 'text') => {
        // 只处理 ListModels 的响应
        if (!xhr._isListModelsXHR || xhr.readyState !== 4 || xhr.status !== 200) {
            return originalValue;
        }

        const cacheKey = '_modifiedResponseCache_' + type;

        if (xhr[cacheKey] === undefined) {
            const originalText = (type === 'text' || typeof originalValue !== 'object' || originalValue === null)
                ? String(originalValue || '')
                : JSON.stringify(originalValue);

            // 使用修改 ListModels 响应的函数
            xhr[cacheKey] = modifyListModelsResponseBody(originalText, xhr._interceptorUrl);
        }

        const cachedResponse = xhr[cacheKey];

        try {
            if (type === 'json' && typeof cachedResponse === 'string') {
                const textToParse = cachedResponse.replace(ANTI_HIJACK_PREFIX, '');
                return textToParse ? JSON.parse(textToParse) : null;
            }
        } catch (e) {
            console.error(LOG_PREFIX, '[XHR] 解析缓存的 JSON 时出错:', e);
            return originalValue;
        }

        return cachedResponse;
    };

    // 重写 responseText 属性
    if (originalResponseTextDescriptor?.get) {
        Object.defineProperty(xhrProto, 'responseText', {
            get: function() {
                const originalText = originalResponseTextDescriptor.get.call(this);

                if (this.responseType && this.responseType !== 'text' && this.responseType !== "") {
                    return originalText;
                }

                return handleXHRResponse(this, originalText, 'text');
            },
            configurable: true
        });
    }

    // 重写 response 属性
    if (originalResponseDescriptor?.get) {
        Object.defineProperty(xhrProto, 'response', {
            get: function() {
                const originalResponse = originalResponseDescriptor.get.call(this);

                if (this.responseType === 'json') {
                    return handleXHRResponse(this, originalResponse, 'json');
                }

                if (!this.responseType || this.responseType === 'text' || this.responseType === "") {
                    return handleXHRResponse(this, originalResponse, 'text');
                }

                return originalResponse;
            },
            configurable: true
        });
    }

    console.log(LOG_PREFIX, '脚本已激活。ListModels 响应拦截和 CountTokens 请求拦截已启用。');
})();