diff --git a/aistudio-testmodel.js b/aistudio-testmodel.js new file mode 100644 index 0000000..3ea215f --- /dev/null +++ b/aistudio-testmodel.js @@ -0,0 +1,406 @@ +// ==UserScript== +// @name Google AI Studio 模型注入器(多模型版) +// @namespace http://tampermonkey.net/ +// @version 1.6.1 +// @description 向 Google AI Studio 注入自定义模型,支持主题表情图标。拦截 XHR/Fetch 请求,处理数组结构的 JSON 数据 +// @author Generated by AI / HCPTangHY / Mozi / wisdgod +// @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.1"; + const LOG_PREFIX = `[AI Studio 注入器 ${SCRIPT_VERSION}]`; + const ANTI_HIJACK_PREFIX = ")]}'\n"; + + // 模型配置列表 + const MODELS_TO_INJECT = [ + { + name: 'models/kingfall-ab-test', + displayName: `👑 Kingfall (脚本 ${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 是否为目标 API 端点 + * @param {string} url - 要检查的 URL + * @returns {boolean} + */ + function isTargetURL(url) { + return url && typeof url === 'string' && + url.includes('alkalimakersuite') && + url.includes('/ListModels'); + } + + /** + * 递归查找模型列表数组 + * @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+(-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}`); + } else { + // 标记为原始模型 + existingModel[MODEL_FIELDS.DISPLAY_NAME] = modelToInject.displayName + " (原始)"; + console.log(LOG_PREFIX, `已更新官方模型 ${modelToInject.name} 的显示名称`); + } + return true; + } + + /** + * 创建新模型 + * @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} 结构)`; + + if (!Array.isArray(newModel[MODEL_FIELDS.METHODS])) { + newModel[MODEL_FIELDS.METHODS] = [ + "generateContent", + "countTokens", + "createCachedContent", + "batchGenerateContent" + ]; + } + + return newModel; + } + + // ==================== 核心处理函数 ==================== + + /** + * 处理并修改 JSON 数据 + * @param {Object} jsonData - 原始 JSON 数据 + * @param {string} url - 请求 URL + * @returns {Object} 包含处理后数据和修改标志的对象 + */ + function processJsonData(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); + modificationMade = true; + console.log(LOG_PREFIX, `成功注入: ${modelToInject.displayName}`); + } else { + // 更新现有模型 + if (updateExistingModel(existingModel, modelToInject)) { + modificationMade = true; + } + } + }); + + return { data: jsonData, modified: modificationMade }; + } + + /** + * 修改响应体 + * @param {string} originalText - 原始响应文本 + * @param {string} url - 请求 URL + * @returns {string} 修改后的响应文本 + */ + 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 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] 拦截到目标请求:', 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] 处理错误:', e); + } + } + return response; + }; + + // 拦截 XMLHttpRequest + const xhrProto = XMLHttpRequest.prototype; + const originalOpen = xhrProto.open; + const originalResponseTextDescriptor = Object.getOwnPropertyDescriptor(xhrProto, 'responseText'); + const originalResponseDescriptor = Object.getOwnPropertyDescriptor(xhrProto, 'response'); + let interceptionCount = 0; + + // 重写 open 方法 + xhrProto.open = function(method, url) { + this._interceptorUrl = url; + this._isTargetXHR = isTargetURL(url); + + if (this._isTargetXHR) { + interceptionCount++; + console.log(LOG_PREFIX, `[XHR] 检测到目标请求 (${interceptionCount}):`, url); + } + + return originalOpen.apply(this, arguments); + }; + + /** + * 处理 XHR 响应 + * @param {XMLHttpRequest} xhr - XHR 对象 + * @param {any} originalValue - 原始响应值 + * @param {string} type - 响应类型 + * @returns {any} 处理后的响应值 + */ + 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; + }; + + // 重写 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, '脚本已激活,Fetch 和 XHR 拦截已启用'); +})();