// ==UserScript==
// @name Grok Monitor
// @namespace https://github.com/Loongphy/Grok-Monitor
// @version 1.0.0
// @author Loongphy
// @description 监控 Grok API 配额(标准、思考、深度、更深),默认显示总数,悬浮查看详情
// @match https://grok.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @license GPL-3.0
// ==/UserScript==
/*
* Grok Monitor - 监控 Grok API 配额使用情况的油猴脚本
*
* 本脚本基于 GPL-3.0 许可证
*
* 这是一个衍生作品,基于 BlueSkyXN 的 Grok Helper
* 原始代码: https://github.com/BlueSkyXN/GPT-Models-Plus/blob/main/GrokHelper.js
* 原作者: BlueSkyXN
*/
(function() {
'use strict';
// 缓存查询结果
let cachedResults = null;
// 获取用户设置或设置默认值
let isCompactMode = GM_getValue('compactMode', true); // 默认使用精简模式
// 四种模式 -> 中文名称对应表
const MODE_LABELS = {
DEFAULT: '标准',
REASONING: '思考',
DEEPSEARCH: '深度',
DEEPERSEARCH: '更深'
};
// 我们需要查询的四种模式
const REQUEST_KINDS = Object.keys(MODE_LABELS);
// 图标 SVG (来自 Font Awesome 免费图标)
const ICONS = {
BOLT: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="16" height="16"><path fill="currentColor" d="M349.4 44.6c5.9-13.7 1.5-29.7-10.6-38.5s-28.6-8-39.9 1.8l-256 224c-10 8.8-13.6 22.9-8.9 35.3S50.7 288 64 288H175.5L98.6 467.4c-5.9 13.7-1.5 29.7 10.6 38.5s28.6 8 39.9-1.8l256-224c10-8.8 13.6-22.9 8.9-35.3S397.3 224 384 224H272.5L349.4 44.6z"/></svg>',
TIMER: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="16" height="16"><path fill="currentColor" d="M464 256A208 208 0 1 1 48 256a208 208 0 1 1 416 0zM0 256a256 256 0 1 0 512 0A256 256 0 1 0 0 256zM232 120V256c0 8 4 15.5 10.7 20l96 64c11 7.4 25.9 4.4 33.3-6.7s4.4-25.9-6.7-33.3L280 243.2V120c0-13.3-10.7-24-24-24s-24 10.7-24 24z"/></svg>',
INFO: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="14" height="14"><path fill="currentColor" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>',
REFRESH: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="12" height="12"><path fill="currentColor" d="M463.5 224H472c13.3 0 24-10.7 24-24V72c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8H463.5z"/></svg>'
};
// 添加自定义样式
GM_addStyle(`
/* 通用样式 */
.grok-monitor {
position: fixed;
left: 16px;
top: 72px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
z-index: 100;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
width: fit-content;
}
/* 完整模式样式 */
.grok-monitor.full-mode {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
border-radius: 12px;
background-color: rgba(255, 255, 255, 0.95);
color: #333;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(8px);
max-width: 280px;
border: 1px solid rgba(0, 0, 0, 0.06);
transform-origin: top left;
padding: 12px 16px;
font-size: 14px;
}
.grok-monitor.full-mode:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12), 0 2px 5px rgba(0, 0, 0, 0.1);
}
/* 精简模式样式 */
.grok-monitor.compact-mode {
display: flex;
flex-direction: column;
border-radius: 12px;
background-color: rgba(255, 255, 255, 0.95);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.06);
overflow: hidden;
max-width: 280px;
}
.grok-monitor.compact-mode .compact-header {
display: flex;
align-items: center;
padding: 8px 16px;
gap: 10px;
font-size: 15px;
color: #333;
width: 100%;
}
.grok-monitor-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
gap: 10px;
}
.grok-monitor-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
color: #444;
}
.grok-monitor-title .icon {
display: flex;
align-items: center;
color: #2563EB;
}
.grok-monitor-summary {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
font-weight: 500;
font-size: 15px;
color: #444;
width: 100%;
}
.full-mode .grok-monitor-summary {
background: rgba(0, 0, 0, 0.03);
padding: 6px 10px;
border-radius: 8px;
justify-content: space-between;
}
.compact-mode .grok-monitor-summary {
padding: 0;
margin: 0;
justify-content: space-between;
}
.grok-monitor-summary-text {
display: flex;
align-items: center;
gap: 6px;
}
.grok-monitor-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
transition: all 0.3s ease;
margin-left: 8px;
}
.grok-monitor-indicator.green {
background-color: #10B981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
}
.grok-monitor-indicator.yellow {
background-color: #F59E0B;
box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
}
.grok-monitor-indicator.red {
background-color: #EF4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
}
.grok-monitor-details {
display: none;
flex-direction: column;
gap: 8px;
font-size: 13px;
color: #555;
width: 100%;
}
.show-details .grok-monitor-details,
.full-mode:hover .grok-monitor-details,
.compact-mode:hover .grok-monitor-details {
display: flex;
animation: fadeIn 0.3s ease forwards;
}
.compact-mode .grok-monitor-details {
padding: 0 16px 12px;
}
.grok-monitor-kind-row {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
padding: 6px 10px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.02);
justify-content: space-between;
transition: background-color 0.2s ease;
}
.grok-monitor-kind-row:hover {
background: rgba(0, 0, 0, 0.04);
}
.grok-monitor-kind-name {
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 6px;
}
.grok-monitor-kind-name .icon {
display: flex;
color: #555;
}
.grok-monitor-info {
color: #666;
display: flex;
align-items: center;
gap: 4px;
}
.grok-monitor-info .time {
opacity: 0.8;
font-size: 12px;
color: #777;
display: flex;
align-items: center;
gap: 3px;
}
.grok-monitor-info .time .icon {
display: flex;
color: #777;
}
.refresh-button {
display: flex;
align-items: center;
justify-content: center;
background: rgba(37, 99, 235, 0.08);
color: #2563EB;
border: none;
border-radius: 6px;
width: 24px;
height: 24px;
cursor: pointer;
transition: background-color 0.3s ease;
padding: 0;
margin-left: auto;
}
.refresh-button:hover {
background: rgba(37, 99, 235, 0.15);
}
.refresh-button .icon-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.refresh-button:hover .icon-container {
animation: rotateIcon 0.8s ease forwards;
}
.grok-monitor.updating .grok-monitor-indicator {
animation: pulse 1s ease-in-out infinite;
}
.mode-switch-btn {
width: 100%;
padding: 6px 0;
font-size: 13px;
border: none;
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
cursor: pointer;
color: #555;
transition: background 0.2s ease;
margin-top: 4px;
}
.mode-switch-btn:hover {
background: rgba(0, 0, 0, 0.05);
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.3);
opacity: 0.7;
}
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes rotateIcon {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-color-scheme: dark) {
.grok-monitor.full-mode,
.grok-monitor.compact-mode {
background-color: rgba(30, 30, 30, 0.9);
color: #eee;
border-color: rgba(255, 255, 255, 0.1);
}
.mode-switch-btn {
background: rgba(255, 255, 255, 0.05);
color: #aaa;
}
.mode-switch-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.grok-monitor-title {
color: #ddd;
}
.grok-monitor-summary {
color: #ddd;
}
.full-mode .grok-monitor-summary {
background: rgba(255, 255, 255, 0.05);
}
.grok-monitor-details {
color: #ccc;
}
.grok-monitor-kind-row {
background: rgba(255, 255, 255, 0.03);
}
.grok-monitor-kind-row:hover {
background: rgba(255, 255, 255, 0.07);
}
.grok-monitor-kind-name {
color: #ddd;
}
.grok-monitor-kind-name .icon {
color: #aaa;
}
.grok-monitor-info {
color: #bbb;
}
.grok-monitor-info .time {
color: #999;
}
.grok-monitor-info .time .icon {
color: #999;
}
.refresh-button {
background: rgba(59, 130, 246, 0.12);
}
.refresh-button:hover {
background: rgba(59, 130, 246, 0.2);
}
}
`);
// 工具函数:格式化等待时间
function formatWaitTime(seconds) {
if (seconds <= 0) return '0分';
const minutes = Math.floor(seconds / 60);
return `${minutes}分`;
}
// 工具函数:格式化窗口时间
function formatWindowTime(seconds) {
if (seconds <= 0) return '0h';
const hours = Math.floor(seconds / 3600);
return `${hours}h`;
}
// 切换显示模式
function toggleMode() {
isCompactMode = !isCompactMode;
GM_setValue('compactMode', isCompactMode);
// 直接更新UI,不刷新页面
const monitorElement = document.querySelector('.grok-monitor');
if (monitorElement) {
// 清空现有内容
monitorElement.innerHTML = '';
monitorElement.className = `grok-monitor ${isCompactMode ? 'compact-mode' : 'full-mode'}`;
// 重新创建UI
if (isCompactMode) {
createCompactModeUI(monitorElement);
} else {
createFullModeUI(monitorElement);
}
// 如果有缓存的数据,直接更新UI
if (cachedResults) {
updateUI(cachedResults);
}
}
}
// 创建监控器UI
function createMonitor() {
const monitor = document.createElement('div');
monitor.className = `grok-monitor ${isCompactMode ? 'compact-mode' : 'full-mode'}`;
if (isCompactMode) {
// 精简模式
createCompactModeUI(monitor);
} else {
// 完整模式
createFullModeUI(monitor);
}
document.body.appendChild(monitor);
return monitor;
}
// 创建完整模式UI
function createFullModeUI(monitor) {
// 标题栏
const header = document.createElement('div');
header.className = 'grok-monitor-header';
const title = document.createElement('div');
title.className = 'grok-monitor-title';
const titleIcon = document.createElement('span');
titleIcon.className = 'icon';
titleIcon.innerHTML = ICONS.BOLT;
const titleText = document.createElement('span');
titleText.textContent = 'Grok 配额监控';
title.appendChild(titleIcon);
title.appendChild(titleText);
const refreshButton = document.createElement('button');
refreshButton.className = 'refresh-button';
refreshButton.title = '刷新数据';
// 创建图标容器,旋转将只应用于此容器
const iconContainer = document.createElement('span');
iconContainer.className = 'icon-container';
iconContainer.innerHTML = ICONS.REFRESH;
refreshButton.appendChild(iconContainer);
refreshButton.onclick = async (e) => {
e.stopPropagation();
await checkRateLimits();
};
header.appendChild(title);
header.appendChild(refreshButton);
// 小版本(默认显示)
const summaryRow = document.createElement('div');
summaryRow.className = 'grok-monitor-summary';
const sumText = document.createElement('div');
sumText.className = 'grok-monitor-summary-text';
const sumSpan = document.createElement('span');
sumSpan.textContent = '剩余总数: ...';
sumText.appendChild(sumSpan);
const indicator = document.createElement('div');
indicator.className = 'grok-monitor-indicator';
summaryRow.appendChild(sumText);
summaryRow.appendChild(indicator);
// 大版本(悬浮后展开)
const details = document.createElement('div');
details.className = 'grok-monitor-details';
// 为每种模式创建行
REQUEST_KINDS.forEach(kind => {
const row = document.createElement('div');
row.className = 'grok-monitor-kind-row';
const nameContainer = document.createElement('div');
nameContainer.className = 'grok-monitor-kind-name';
const kindIcon = document.createElement('span');
kindIcon.className = 'icon';
kindIcon.innerHTML = ICONS.INFO;
const nameSpan = document.createElement('span');
nameSpan.textContent = MODE_LABELS[kind];
nameContainer.appendChild(kindIcon);
nameContainer.appendChild(nameSpan);
const infoContainer = document.createElement('div');
infoContainer.className = 'grok-monitor-info';
const infoSpan = document.createElement('span');
infoSpan.textContent = '剩余 .../...';
const timeSpan = document.createElement('span');
timeSpan.className = 'time';
const timeIcon = document.createElement('span');
timeIcon.className = 'icon';
timeIcon.innerHTML = ICONS.TIMER;
const timeText = document.createElement('span');
timeText.textContent = '...h刷新';
timeSpan.appendChild(timeIcon);
timeSpan.appendChild(timeText);
infoContainer.appendChild(infoSpan);
infoContainer.appendChild(timeSpan);
row.appendChild(nameContainer);
row.appendChild(infoContainer);
details.appendChild(row);
});
// 添加切换模式按钮
const switchBtn = document.createElement('button');
switchBtn.className = 'mode-switch-btn';
switchBtn.textContent = '切换为精简模式';
switchBtn.onclick = toggleMode;
details.appendChild(switchBtn);
monitor.appendChild(header);
monitor.appendChild(summaryRow);
monitor.appendChild(details);
}
// 创建精简模式UI
function createCompactModeUI(monitor) {
// 创建精简头部
const compactHeader = document.createElement('div');
compactHeader.className = 'compact-header';
// 精简模式只显示总数和指示灯
const summaryRow = document.createElement('div');
summaryRow.className = 'grok-monitor-summary';
const sumText = document.createElement('div');
sumText.className = 'grok-monitor-summary-text';
const sumSpan = document.createElement('span');
sumSpan.textContent = '剩余总数: ...';
sumText.appendChild(sumSpan);
const indicator = document.createElement('div');
indicator.className = 'grok-monitor-indicator';
// 添加刷新按钮
const refreshButton = document.createElement('button');
refreshButton.className = 'refresh-button';
refreshButton.title = '刷新数据';
const iconContainer = document.createElement('span');
iconContainer.className = 'icon-container';
iconContainer.innerHTML = ICONS.REFRESH;
refreshButton.appendChild(iconContainer);
refreshButton.onclick = async (e) => {
e.stopPropagation();
await checkRateLimits();
};
summaryRow.appendChild(sumText);
summaryRow.appendChild(indicator);
summaryRow.appendChild(refreshButton);
compactHeader.appendChild(summaryRow);
// 创建详情部分(悬浮时展开)
const details = document.createElement('div');
details.className = 'grok-monitor-details';
// 为每种模式创建行
REQUEST_KINDS.forEach(kind => {
const row = document.createElement('div');
row.className = 'grok-monitor-kind-row';
const nameContainer = document.createElement('div');
nameContainer.className = 'grok-monitor-kind-name';
const kindIcon = document.createElement('span');
kindIcon.className = 'icon';
kindIcon.innerHTML = ICONS.INFO;
const nameSpan = document.createElement('span');
nameSpan.textContent = MODE_LABELS[kind];
nameContainer.appendChild(kindIcon);
nameContainer.appendChild(nameSpan);
const infoContainer = document.createElement('div');
infoContainer.className = 'grok-monitor-info';
const infoSpan = document.createElement('span');
infoSpan.textContent = '剩余 .../...';
const timeSpan = document.createElement('span');
timeSpan.className = 'time';
const timeIcon = document.createElement('span');
timeIcon.className = 'icon';
timeIcon.innerHTML = ICONS.TIMER;
const timeText = document.createElement('span');
timeText.textContent = '...h刷新';
timeSpan.appendChild(timeIcon);
timeSpan.appendChild(timeText);
infoContainer.appendChild(infoSpan);
infoContainer.appendChild(timeSpan);
row.appendChild(nameContainer);
row.appendChild(infoContainer);
details.appendChild(row);
});
// 添加切换模式按钮
const switchBtn = document.createElement('button');
switchBtn.className = 'mode-switch-btn';
switchBtn.textContent = '切换为完整模式';
switchBtn.onclick = toggleMode;
details.appendChild(switchBtn);
monitor.appendChild(compactHeader);
monitor.appendChild(details);
}
// 获取当前域名的基础URL
function getBaseUrl() {
return window.location.origin;
}
// 获取每种模式的限额
async function fetchRateLimit(kind) {
try {
const baseUrl = getBaseUrl();
const response = await fetch(`${baseUrl}/rest/rate-limits`, {
method: 'POST',
headers: {
'accept': '*/*',
'content-type': 'application/json'
},
body: JSON.stringify({
requestKind: kind,
modelName: "grok-3"
}),
credentials: 'include'
});
if (response.ok) {
return await response.json();
} else {
throw new Error(`Failed to fetch ${kind} rate limit`);
}
} catch (error) {
console.error('Rate limit check failed:', error);
return null;
}
}
// 一次获取所有模式数据
async function getAllRateLimits() {
const results = {};
for (const kind of REQUEST_KINDS) {
results[kind] = await fetchRateLimit(kind);
}
return results;
}
// 更新UI
function updateUI(results) {
// 缓存结果以便稍后使用
cachedResults = results;
const monitor = document.querySelector('.grok-monitor');
const sumSpan = monitor.querySelector('.grok-monitor-summary-text span');
const indicator = monitor.querySelector('.grok-monitor-indicator');
monitor.classList.add('updating');
// 移除之前的指示器类
indicator.classList.remove('green', 'yellow', 'red');
// 计算合计剩余次数
let sum = 0;
REQUEST_KINDS.forEach(kind => {
const data = results[kind];
if (data && data.remainingQueries > 0) {
sum += data.remainingQueries;
}
});
// 更新总数
sumSpan.textContent = `剩余总数: ${sum}`;
// 指示灯颜色
if (sum === 0) {
indicator.classList.add('red');
} else if (sum > 0 && sum < 5) {
indicator.classList.add('yellow');
} else {
indicator.classList.add('green');
}
// 更新详情行
const detailRows = monitor.querySelectorAll('.grok-monitor-details .grok-monitor-kind-row');
updateDetailRows(detailRows, results);
setTimeout(() => monitor.classList.remove('updating'), 1000);
}
// 更新详情行
function updateDetailRows(detailRows, results) {
detailRows.forEach(row => {
const label = row.querySelector('.grok-monitor-kind-name span:last-child')?.textContent;
if (!label) return; // 跳过非模式行
const kind = Object.keys(MODE_LABELS).find(k => MODE_LABELS[k] === label);
if (!kind) return; // 跳过非模式行
const data = results[kind];
const infoSpan = row.querySelector('.grok-monitor-info span:first-child');
const timeText = row.querySelector('.grok-monitor-info .time span:last-child');
if (!data) {
infoSpan.textContent = '获取失败';
timeText.textContent = '';
return;
}
const { remainingQueries, totalQueries, windowSizeSeconds, waitTimeSeconds } = data;
if (remainingQueries > 0) {
infoSpan.textContent = `剩余 ${remainingQueries}/${totalQueries}`;
timeText.textContent = formatWindowTime(windowSizeSeconds);
} else {
infoSpan.textContent = `等待刷新`;
timeText.textContent = formatWaitTime(waitTimeSeconds);
}
});
}
// 定时检查
async function checkRateLimits() {
const results = await getAllRateLimits();
updateUI(results);
}
// 注册油猴菜单
GM_registerMenuCommand("切换显示模式", toggleMode);
// 初始化
function init() {
createMonitor();
checkRateLimits();
setInterval(checkRateLimits, 30000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();