[JavaScript] 纯文本查看 复制代码
// ==UserScript==
// @name Gemini 用量看板
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 全账号URL动态嗅探,原生DOM+Observer防擦除,右上角常驻
// @AuThor Gemini
// @match https://gemini.google.com/*
// @run-at document-start
// @grant unsafeWindow
// ==/UserScript==
(function() {
'use strict';
const PANEL_ID = 'gemini-usage-panel-v12';
const MENU_ID = 'gemini-usage-menu-v12';
let pollInterval = parseInt(localStorage.getItem('gemini_usage_interval')) || 10;
let pollingTimer = null;
let shortValNode, shortTimeNode, longValNode, longTimeNode, statusNode;
// 动态存储真实的 API 地址(包含 /u/1/ 等路由)
let realApiUrl = 'https://gemini.google.com/_/BardChatUi/data/batchexecute?rpcids=jSf9Qc&rt=c';
// --- 1. 底层拦截:精准偷取真实路由 URL ---
const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const origFetch = win.fetch;
win.fetch = async function(...args) {
if (typeof args[0] === 'string' && args[0].includes('batchexecute')) {
try {
const urlObj = new URL(args[0], location.origin);
urlObj.searchParams.set('rpcids', 'jSf9Qc');
realApiUrl = urlObj.toString();
} catch(e) {}
}
return origFetch.apply(this, args);
};
const origXhrOpen = win.XMLHttpRequest.prototype.open;
win.XMLHttpRequest.prototype.open = function(method, url) {
if (typeof url === 'string' && url.includes('batchexecute')) {
try {
const urlObj = new URL(url, location.origin);
urlObj.searchParams.set('rpcids', 'jSf9Qc');
realApiUrl = urlObj.toString();
} catch(e) {}
}
return origXhrOpen.apply(this, arguments);
};
// --- 2. 界面构建 ---
function el(tag, styles = {}, text = '') {
const e = document.createElement(tag);
Object.assign(e.style, styles);
if (text) e.textContent = text;
return e;
}
function initUI() {
if (document.getElementById(PANEL_ID)) return;
if (!document.body) return;
const panel = el('div', {
position: 'fixed', top: '80px', right: '20px', background: 'rgba(25, 25, 25, 0.95)',
color: '#e8eaed', padding: '15px', borderRadius: '12px', fontFamily: 'sans-serif',
fontSize: '13px', minWidth: '170px', boxShadow: '0 4px 15px rgba(0,0,0,0.5)',
border: '1px solid rgba(255,255,255,0.1)', backdropFilter: 'blur(10px)',
zIndex: '2147483647', userSelect: 'none', cursor: 'context-menu'
});
panel.id = PANEL_ID;
const titleRow = el('div', { display: 'flex', justifyContent: 'space-between', borderBottom: '1px solid #444', paddingBottom: '8px', marginBottom: '12px' });
titleRow.appendChild(el('span', { fontWeight: 'bold', color: '#fff' }, '📊 用量看板'));
statusNode = el('span', { color: '#aa0000', fontSize: '12px', title: '初始化...' }, '●');
titleRow.appendChild(statusNode);
panel.appendChild(titleRow);
function createSec(labelName) {
const sec = el('div', { marginBottom: '10px', background: 'rgba(0,0,0,0.2)', padding: '8px', borderRadius: '8px' });
const row = el('div', { display: 'flex', justifyContent: 'space-between', marginBottom: '4px' });
row.appendChild(el('span', { color: '#9aa0a6' }, labelName));
const val = el('span', { fontWeight: 'bold', color: '#8ab4f8' }, '--%');
row.appendChild(val);
const time = el('div', { color: '#5f6368', fontSize: '11px', textAlign: 'left' }, '刷新: --');
sec.appendChild(row); sec.appendChild(time);
panel.appendChild(sec);
return { val, time };
}
const sData = createSec('5小时用量:');
shortValNode = sData.val; shortTimeNode = sData.time;
const lData = createSec('本周用量:');
longValNode = lData.val; longTimeNode = lData.time;
document.body.appendChild(panel);
const menu = el('div', {
position: 'fixed', background: '#2c2c2c', color: '#fff', padding: '5px 0',
borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.6)', border: '1px solid #555',
zIndex: '2147483647', display: 'none', fontSize: '13px', minWidth: '120px'
});
menu.id = MENU_ID;
const btnRef = el('div', { padding: '8px 15px', cursor: 'pointer' }, '🔄 立即刷新');
const btnSet = el('div', { padding: '8px 15px', cursor: 'pointer' }, '⚙️ 设置周期');
const hIn = (e) => e.target.style.background = '#444';
const hOut = (e) => e.target.style.background = 'transparent';
btnRef.onmouseover = hIn; btnRef.onmouseout = hOut;
btnSet.onmouseover = hIn; btnSet.onmouseout = hOut;
menu.appendChild(btnRef); menu.appendChild(btnSet);
document.body.appendChild(menu);
panel.addEventListener('contextmenu', (e) => {
e.preventDefault();
menu.style.display = 'block';
menu.style.left = e.clientX + 'px';
menu.style.top = e.clientY + 'px';
});
document.addEventListener('click', () => menu.style.display = 'none');
btnRef.addEventListener('click', fetchUsage);
btnSet.addEventListener('click', () => {
const cur = localStorage.getItem('gemini_usage_interval') || 10;
const ipt = prompt('请输入采集周期 (秒):', cur);
if (ipt && !isNaN(ipt) && parseInt(ipt) > 0) {
pollInterval = parseInt(ipt);
localStorage.setItem('gemini_usage_interval', pollInterval);
startPolling();
}
});
}
// --- 3. Observer 防擦除守护 ---
const observer = new MutationObserver(() => {
if (document.body && !document.getElementById(PANEL_ID)) {
initUI();
if(shortValNode) fetchUsage(); // 重建后立刻刷新一次数据
}
});
// --- 4. 核心逻辑 ---
function formatTime(ts) {
if(!ts) return '--';
return new Date(ts * 1000).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute:'2-digit' });
}
function getToken() {
try { if (win.WIZ_global_data?.SNlM0e) return win.WIZ_global_data.SNlM0e; } catch(e) {}
const match = document.documentElement.outerHTML.match(/"SNlM0e":"([^"]+)"/);
return match ? match[1] : null;
}
async function fetchUsage() {
if (!shortValNode || !longValNode) return;
const token = getToken();
if (!token) {
if (statusNode) { statusNode.style.color = '#ff8a65'; statusNode.title = 'Token 提取失败'; }
return;
}
if (statusNode) statusNode.style.color = '#aecbfa';
const payload = new URLSearchParams({ 'f.req': '[[["jSf9Qc","[]",null,"generic"]]]', 'at': token });
try {
const res = await win.fetch(realApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body: payload.toString() });
let text = await res.text();
if (text.startsWith(")]}'")) text = text.substring(4);
const lines = text.split('\n');
let dataFound = false;
for (let line of lines) {
if (line.includes('jSf9Qc') && line.includes('wrb.fr')) {
const parsedLine = JSON.parse(line);
const quotas = JSON.parse(parsedLine[0][2])[1];
let shortTerm = null, longTerm = null;
for (let q of quotas) {
if (q[2] === 1) shortTerm = q;
if (q[2] === 2) longTerm = q;
}
if (shortTerm) {
shortValNode.textContent = (shortTerm[1] * 100).toFixed(2) + '%';
shortTimeNode.textContent = '刷新: ' + formatTime(shortTerm[3][0][0]);
}
if (longTerm) {
longValNode.textContent = (longTerm[1] * 100).toFixed(2) + '%';
longTimeNode.textContent = '刷新: ' + formatTime(longTerm[3][0][0]);
}
dataFound = true;
break;
}
}
if (statusNode) {
statusNode.style.color = dataFound ? '#00cc00' : '#ff8a65';
statusNode.title = dataFound ? `同步成功: ${new Date().toLocaleTimeString()}` : '数据异常 (可能需对话一次以唤醒)';
}
} catch (error) {
if (statusNode) { statusNode.style.color = '#ff3333'; statusNode.title = '网络连接失败'; }
}
}
function startPolling() {
if (pollingTimer) clearInterval(pollingTimer);
pollingTimer = setInterval(fetchUsage, pollInterval * 1000);
}
// 初始化器
function boot() {
if (document.body) {
initUI();
observer.observe(document.body, { childList: true, subtree: true });
fetchUsage();
startPolling();
} else {
requestAnimationFrame(boot);
}
}
boot();
})();