// ==UserScript==
// @name Gemini Always Pro
// @namespace https://github.com/lzcmian/gemini-always-pro
// @version 0.5.1
// @description Keep Gemini locked to the Pro model via structural selectors.
// @author lzcmian
// @match https://gemini.google.com/*
// @match https://bard.google.com/*
// @run-at document-idle
// @grant GM_registerMenuCommand
// ==/UserScript==
(() => {
"use strict";
const STORAGE_KEY = "gemini-always-pro.lock-enabled";
const TOGGLE_ID = "gemini-always-pro-toggle";
const STYLE_ID = "gemini-always-pro-style";
const WARN_ATTR = "data-gap-warn";
const MODE_BUTTON = '[data-test-id="bard-mode-menu-button"]';
const PRO_OPTION = '[data-test-id="bard-mode-option-pro"]';
const DEBOUNCE_MS = 120;
const RUN_GAP_MS = 1500;
const MENU_SETTLE_MS = 300;
const USER_QUIET_MS = 1200;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let enabled = localStorage.getItem(STORAGE_KEY) !== "0";
let busyUntil = 0;
let pending = 0;
let lastUserInputAt = 0;
let syntheticDepth = 0;
const modeButton = () => document.querySelector(MODE_BUTTON);
const isProNow = () => modeButton()?.innerText.trim().toLowerCase() === "pro";
const userBusy = () => {
const a = document.activeElement;
if (!a?.matches?.('input, textarea, [contenteditable="true"], [contenteditable="plaintext-only"]')) return false;
if (Date.now() - lastUserInputAt < USER_QUIET_MS) return true;
return Boolean(a.value?.trim() || a.textContent?.replace(//g, "").trim());
};
const renderToggle = () => {
let btn = document.getElementById(TOGGLE_ID);
if (!btn) {
btn = document.createElement("button");
btn.id = TOGGLE_ID;
btn.type = "button";
btn.style.cssText =
"all:initial;position:fixed;right:14px;bottom:14px;z-index:2147483647;" +
"min-width:108px;padding:9px 13px;border:0;border-radius:999px;color:#fff;" +
'font:700 13px/1.2 system-ui,-apple-system,"Segoe UI",sans-serif;cursor:pointer;' +
"box-shadow:0 6px 22px rgba(0,0,0,.28);user-select:none";
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
enabled = !enabled;
localStorage.setItem(STORAGE_KEY, enabled ? "1" : "0");
renderToggle();
if (enabled) schedule();
});
(document.body || document.documentElement).append(btn);
}
btn.textContent = enabled ? "Pro 锁定:开" : "Pro 锁定:关";
btn.style.background = enabled ? "#0b57d0" : "#5f6368";
btn.setAttribute("aria-pressed", String(enabled));
};
const ensureWarningStyle = () => {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
[data-test-id="bard-mode-menu-button"][${WARN_ATTR}="true"] {
background-color: #d93025 !important;
color: #fff !important;
box-shadow: 0 0 0 2px rgba(217,48,37,.55), 0 0 12px rgba(217,48,37,.45) !important;
border-radius: 999px !important;
transition: background-color .15s ease, box-shadow .15s ease;
}
[data-test-id="bard-mode-menu-button"][${WARN_ATTR}="true"] *,
[data-test-id="bard-mode-menu-button"][${WARN_ATTR}="true"] mat-icon {
color: #fff !important;
fill: #fff !important;
}
`;
(document.head || document.documentElement).append(style);
};
const updateWarning = () => {
const btn = modeButton();
if (!btn) return;
if (isProNow()) btn.removeAttribute(WARN_ATTR);
else btn.setAttribute(WARN_ATTR, "true");
};
async function ensurePro() {
if (!enabled || Date.now() < busyUntil) return;
if (isProNow()) return;
if (userBusy()) {
schedule(USER_QUIET_MS);
return;
}
const btn = modeButton();
if (!btn) return;
busyUntil = Date.now() + RUN_GAP_MS;
syntheticDepth += 1;
try {
btn.click();
await sleep(MENU_SETTLE_MS);
const pro = document.querySelector(PRO_OPTION);
if (pro) pro.click();
else document.body.click();
} finally {
syntheticDepth -= 1;
}
}
const schedule = (delay = DEBOUNCE_MS) => {
clearTimeout(pending);
pending = setTimeout(ensurePro, delay);
};
const markUserInput = (e) => {
if (syntheticDepth > 0 || e.target?.id === TOGGLE_ID) return;
lastUserInputAt = Date.now();
};
for (const ev of ["pointerdown", "keydown", "input", "compositionstart", "paste"]) {
document.addEventListener(ev, markUserInput, { capture: true, passive: true });
}
new MutationObserver(() => {
updateWarning();
if (enabled) schedule();
}).observe(document.documentElement, {
subtree: true,
childList: true,
characterData: true,
attributes: true,
attributeFilter: ["class", "aria-expanded", "data-test-id"],
});
if (typeof GM_registerMenuCommand === "function") {
GM_registerMenuCommand("切换 Gemini Pro 锁定", () => {
enabled = !enabled;
localStorage.setItem(STORAGE_KEY, enabled ? "1" : "0");
renderToggle();
if (enabled) schedule();
});
}
ensureWarningStyle();
updateWarning();
renderToggle();
schedule();
})();
1 个帖子 - 1 位参与者