Greasy Fork

Google AI Studio 模型注入器

向 Google AI Studio 注入自定义模型,支持在模型列表中手动添加ID(无需输入 models/)。拦截 XHR/Fetch 请求。

// ==UserScript==
// @name         Google AI Studio 模型注入器
// @namespace    http://tampermonkey.net/
// @version      1.7.0
// @description  向 Google AI Studio 注入自定义模型,支持在模型列表中手动添加ID(无需输入 models/)。拦截 XHR/Fetch 请求。
// @author       Generated by AI / HCPTangHY / Mozi / wisdgod / UserModified
// @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.7.0"; // 更新版本号
    const LOG_PREFIX = `[AI Studio 注入器 ${SCRIPT_VERSION}]`;
    const ANTI_HIJACK_PREFIX = ")]}'\n";
    const CUSTOM_MODELS_STORAGE_KEY = 'AI_STUDIO_INJECTOR_CUSTOM_MODELS';

    // 特殊“动作”模型定义
    const ACTION_ADD_MODEL = {
        name: 'models/---script-action-add-custom---',
        displayName: `➕ 添加自定义模型 (点击此处)`,
        description: '点击此项以手动输入新的模型 ID 并保存。'
    };

    const ACTION_CLEAR_MODELS = {
        name: 'models/---script-action-clear-custom---',
        displayName: `🗑️ 清除手动添加的模型 (点击此处)`,
        description: '点击此项以清除所有您手动添加的模型。'
    };

    // 预设模型配置列表
    const PREDEFINED_MODELS = [
        {
            name: 'models/blacktooth-ab-test',
            displayName: `🏴‍☠️ Blacktooth (脚本 ${SCRIPT_VERSION})`,
            description: `由脚本 ${SCRIPT_VERSION} 预设注入的模型`
        },
        {
            name: 'models/jfdksal98a',
            displayName: `🪐 jfdksal98a (脚本 ${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
    };

    // ==================== 自定义模型管理 (UI 逻辑) ====================

    function loadCustomModels() {
        try {
            const storedModels = localStorage.getItem(CUSTOM_MODELS_STORAGE_KEY);
            if (storedModels) {
                const models = JSON.parse(storedModels);
                return models.map(model => ({
                    ...model,
                    displayName: model.displayName.replace(/ \(脚本 v[\d.]+\)$/, '') + ` (脚本 ${SCRIPT_VERSION})`,
                    description: model.description.replace(/脚本 v[\d.]+/, `脚本 ${SCRIPT_VERSION}`)
                }));
            }
        } catch (e) {
            console.error(LOG_PREFIX, '加载自定义模型时出错:', e);
        }
        return [];
    }

    function saveCustomModels(models) {
        try {
            localStorage.setItem(CUSTOM_MODELS_STORAGE_KEY, JSON.stringify(models));
        } catch (e) {
            console.error(LOG_PREFIX, '保存自定义模型时出错:', e);
        }
    }

    /**
     * 提示用户输入新的模型 ID (已修改:自动添加 'models/')
     */
    function promptForNewModel() {
        // 修改提示语,告诉用户不需要输入 'models/'
        const rawInputId = prompt("【添加自定义模型】\n请输入新的模型ID (例如: my-custom-model):\n(会自动添加 'models/' 前缀)");
        if (!rawInputId) return; // 用户取消

        const trimmedId = rawInputId.trim();

        if (!trimmedId) {
            alert("错误:模型ID不能为空。");
            return;
        }

        // 自动添加 'models/' 前缀,如果用户没加的话
        let fullModelId;
        if (trimmedId.startsWith('models/')) {
            fullModelId = trimmedId; // 如果用户还是输入了,也接受
        } else {
            fullModelId = 'models/' + trimmedId;
        }

        // 使用 fullModelId 生成默认名称
        const defaultName = fullModelId.split('/').pop();
        const displayNameInput = prompt("请输入该模型的显示名称 (例如: 🤖 我的模型):", `🤖 ${defaultName}`);
        if (!displayNameInput) return; // 用户取消

        const newModel = {
            name: fullModelId, // 使用完整的 ID
            displayName: `${displayNameInput} (脚本 ${SCRIPT_VERSION})`,
            description: `由用户手动添加并通过脚本 ${SCRIPT_VERSION} 注入的模型`
        };

        const customModels = loadCustomModels();
        const allCurrentModels = [...PREDEFINED_MODELS, ...customModels];

        // 检查完整 ID 是否已存在
        if (allCurrentModels.some(m => m.name === fullModelId)) {
            alert(`错误:模型ID ${fullModelId} 已存在。`);
            return;
        }

        customModels.push(newModel);
        saveCustomModels(customModels);

        alert(`模型 ${fullModelId} 添加成功!\n\n页面将自动刷新以应用更改。`);
        window.location.reload();
    }

    /**
     * 清除所有用户自定义的模型
     */
    function clearAllCustomModels() {
        if (confirm("⚠️ 确定要清除所有您手动添加的自定义模型吗?\n\n(脚本预设的模型不会被删除)")) {
            saveCustomModels([]);
            alert("所有手动添加的自定义模型已清除。页面将自动刷新。");
            window.location.reload();
        }
    }

    /**
     * 拦截列表点击事件
     */
    function setupModelSelectionInterceptor() {
        document.body.addEventListener('click', (event) => {
            const optionElement = event.target.closest('[role="option"], mat-option');

            if (optionElement && optionElement.textContent) {
                const text = optionElement.textContent;
                let actionTaken = false;

                if (text.includes(ACTION_ADD_MODEL.displayName)) {
                    console.log(LOG_PREFIX, "拦截到 '添加模型' 点击事件。");
                    actionTaken = true;
                    setTimeout(promptForNewModel, 50);
                } else if (text.includes(ACTION_CLEAR_MODELS.displayName)) {
                    console.log(LOG_PREFIX, "拦截到 '清除模型' 点击事件。");
                    actionTaken = true;
                    setTimeout(clearAllCustomModels, 50);
                }

                if (actionTaken) {
                    event.preventDefault();
                    event.stopPropagation();
                    if (document.activeElement) {
                        document.activeElement.blur();
                    }
                }
            }
        }, true); // true 表示在捕获阶段处理
        console.log(LOG_PREFIX, '模型选择点击拦截器已设置。');
    }

    // ==================== 初始化 ====================

    const customModels = loadCustomModels();
    const ALL_MODELS_TO_INJECT = [...PREDEFINED_MODELS, ...customModels];

    console.log(LOG_PREFIX, `预设模型: ${PREDEFINED_MODELS.length} 个, 用户自定义模型: ${customModels.length} 个`);

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', setupModelSelectionInterceptor);
    } else {
        setupModelSelectionInterceptor();
    }


    // ==================== 数据处理工具函数 ====================

    function isTargetURL(url) {
        return url && typeof url === 'string' &&
               url.includes('alkalimakersuite') &&
               url.includes('/ListModels');
    }

    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;
    }

    function findTemplateModel(modelsArray) {
        return 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]));
    }

    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+)?\)/, '')
            .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;
            return true; // 已更新,不需要再注入
        } else {
            if (!String(existingModel[MODEL_FIELDS.DISPLAY_NAME]).includes("(原始)")) {
                 existingModel[MODEL_FIELDS.DISPLAY_NAME] += " (原始)";
            }
            return false; // 标记为原始,仍需注入脚本版本
        }
    }

    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} 结构)`;

        if (!Array.isArray(newModel[MODEL_FIELDS.METHODS])) {
            newModel[MODEL_FIELDS.METHODS] = [
                "generateContent", "countTokens", "createCachedContent", "batchGenerateContent"
            ];
        }
        return newModel;
    }

    // ==================== 核心处理函数 ====================

    function processJsonData(jsonData, url) {
        let modificationMade = false;
        const modelsArray = findModelListArray(jsonData);

        if (!modelsArray) return { data: jsonData, modified: false };

        const templateModel = findTemplateModel(modelsArray);
        const templateName = templateModel?.[MODEL_FIELDS.NAME] || 'unknown';

        if (!templateModel) {
            console.warn(LOG_PREFIX, '未找到模板模型');
            return { data: jsonData, modified: false };
        }

        // 1. 注入正常的自定义和预设模型
        [...ALL_MODELS_TO_INJECT].reverse().forEach(modelToInject => {
            const existingModel = modelsArray.find(
                model => Array.isArray(model) && model[MODEL_FIELDS.NAME] === modelToInject.name
            );

            let shouldInjectNew = true;

            if (existingModel) {
               const updated = updateExistingModel(existingModel, modelToInject);
               modificationMade = modificationMade || updated;
               shouldInjectNew = !updated;
            }

            if (shouldInjectNew) {
                const alreadyInjected = modelsArray.find(m => Array.isArray(m) && m[MODEL_FIELDS.NAME] === modelToInject.name && String(m[MODEL_FIELDS.DISPLAY_NAME]).includes('(脚本 v'));
                if (!alreadyInjected) {
                    const newModel = createNewModel(templateModel, modelToInject, templateName);
                    modelsArray.unshift(newModel);
                    modificationMade = true;
                    console.log(LOG_PREFIX, `成功注入: ${modelToInject.displayName}`);
                } else {
                     if (alreadyInjected[MODEL_FIELDS.DISPLAY_NAME] !== modelToInject.displayName) {
                        alreadyInjected[MODEL_FIELDS.DISPLAY_NAME] = modelToInject.displayName;
                        alreadyInjected[MODEL_FIELDS.DESCRIPTION] = `${modelToInject.description} (基于 ${templateName} 结构)`;
                        modificationMade = true;
                     }
                }
            }
        });

        // 2. 注入“动作”条目 (添加和清除)
        const addActionModel = createNewModel(templateModel, ACTION_ADD_MODEL, templateName);
        addActionModel[MODEL_FIELDS.METHODS] = []; // 清空方法列表,让它看起来更像个按钮

        if (customModels.length > 0) {
            const clearActionModel = createNewModel(templateModel, ACTION_CLEAR_MODELS, templateName);
            clearActionModel[MODEL_FIELDS.METHODS] = [];
            modelsArray.unshift(clearActionModel);
        }
        modelsArray.unshift(addActionModel);

        modificationMade = true;
        console.log(LOG_PREFIX, '已注入“添加/清除自定义模型”动作条目。');

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

    function modifyResponseBody(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 = processJsonData(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, '处理响应体时出错:', url, error);
        }
        return originalText;
    }

    // ==================== 请求拦截 ====================

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

        if (isTargetURL(url) && response.ok) {
            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] 处理错误:', e);
            }
        }
        return response;
    };

    // 拦截 XMLHttpRequest
    const xhrProto = XMLHttpRequest.prototype;
    const originalOpen = xhrProto.open;
    const originalResponseTextDescriptor = Object.getOwnPropertyDescriptor(xhrProto, 'responseText');
    const originalResponseDescriptor = Object.getOwnPropertyDescriptor(xhrProto, 'response');

    xhrProto.open = function(method, url) {
        this._interceptorUrl = url;
        this._isTargetXHR = isTargetURL(url);
        return originalOpen.apply(this, arguments);
    };

    const handleXHRResponse = (xhr, originalValue, type = 'text') => {
        if (!xhr._isTargetXHR || 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);
            xhr[cacheKey] = modifyResponseBody(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;
    };

    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
        });
    }

    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, '脚本 v1.7.0 已激活。Fetch 和 XHR 拦截已启用。');
})();