// ==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 拦截已启用。');
})();