基于这位大佬的脚本
注:只能本地统计,多地使用无法统计(但可以导入导出使用记录),抛砖引玉!您的使用就是我更新最大的动力
已上传 Greasyfork ChatGPT用量统计
代码太长只能传Greasyfork了
注:只能本地统计,多地使用无法统计(但可以导入导出使用记录),抛砖引玉!您的使用就是我更新最大的动力
代码太长只能传Greasyfork了
3lue佬怎么这么强
claude怎么这么强
lue佬还是太厉害了
太强了!
太强了,3lue!
太强了 3lue佬
已上传Greasyfork
好好好,看起来更清爽了
啊啊太棒了求发
// ==UserScript==
// @name ChatGPT用量统计(优化)
// @namespace https://github.com/tizee/tampermonkey-chatgpt-model-usage-monitor
// @version 2.2.0
// @description 优化ChatGPT模型调用量实时统计,折叠按钮改为图标简洁样式,悬停显示详细,用量默认显示配额,并增加窗口类型颜色标注
// @author tizee (original), schweigen (modified)
// @match https://chatgpt.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=chatgpt.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// --- 常量配置 ---
const COLORS = {
primary: "#5E9EFF",
background: "#1A1B1E",
surface: "#2A2B2E",
border: "#363636",
text: "#E5E7EB",
secondaryText: "#9CA3AF",
success: "#10B981",
warning: "#F59E0B",
danger: "#EF4444",
disabled: "#4B5563",
white: "#E5E7EB",
gray: "#9CA3AF",
hour3: "#61DAFB", // 3h窗口颜色(天蓝)
daily: "#9F7AEA", // 24h窗口颜色(紫色)
weekly: "#10B981", // 7d窗口颜色(绿色)
};
const TIME_WINDOWS = {
hour3: 3 * 60 * 60 * 1000,
daily: 24 * 60 * 60 * 1000,
weekly: 7 * 24 * 60 * 60 * 1000,
};
const SPECIAL_MODEL = "gpt-4o-mini";
// 默认数据结构(与原脚本保持一致)
const defaultUsageData = {
position: { x: null, y: null },
minimized: false,
models: {
"gpt-4o": {
requests: [],
quota: 80,
windowType: "hour3"
},
"o4-mini": {
requests: [],
quota: 300,
windowType: "daily"
},
"o4-mini-high": {
requests: [],
quota: 100,
windowType: "daily"
},
"o3": {
requests: [],
quota: 100,
windowType: "weekly"
},
"gpt-4": {
requests: [],
quota: 40,
windowType: "hour3"
},
"gpt-4-5": {
requests: [],
quota: 50,
windowType: "weekly"
},
},
miniCount: 0,
progressType: "bar",
showWindowResetTime: false,
};
// --- 数据存储操作 ---
const Storage = {
key: "usageData",
get() {
let usageData = GM_getValue(this.key, defaultUsageData);
if (!usageData) usageData = defaultUsageData;
// 版本兼容及默认属性补充
usageData.position = usageData.position || { x: null, y: null };
usageData.minimized = usageData.minimized ?? false;
usageData.models = usageData.models || {};
usageData.miniCount = usageData.miniCount || 0;
usageData.progressType = usageData.progressType || "bar";
usageData.showWindowResetTime = usageData.showWindowResetTime || false;
// 清理特殊模型数据
if (usageData.models[SPECIAL_MODEL]) {
usageData.miniCount += (usageData.models[SPECIAL_MODEL].requests?.length || 0);
delete usageData.models[SPECIAL_MODEL];
}
GM_setValue(this.key, usageData);
return usageData;
},
set(data) {
GM_setValue(this.key, data);
},
update(callback) {
const data = this.get();
callback(data);
this.set(data);
}
};
let usageData = Storage.get();
// --- 样式 ---
GM_addStyle(`
#chatUsageMonitor {
position: fixed;
top: 20px;
right: 20px;
background: rgba(26, 27, 30, 0.9);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: ${COLORS.text};
font-size: 14px;
user-select: none;
z-index: 999999;
width: fit-content;
max-width: 320px;
overflow: visible;
}
#chatUsageMonitor.minimized {
width: 32px;
height: 32px;
border-radius: 16px;
background: rgba(0, 0, 0, 0.1); /* 更透明的背景 */
color: var(--text-color, #666); /* 使用系统文本颜色或后备色 */
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
backdrop-filter: blur(4px); /* 可选:毛玻璃效果 */
}
/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
#chatUsageMonitor.minimized {
background: rgba(255, 255, 255, 0.1);
color: var(--text-color, #999);
}
}
#chatUsageMonitor .usage-content {
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 8px;
max-height: 60vh;
overflow-y: auto;
pointer-events: auto;
user-select: none;
}
#chatUsageMonitor .model-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
#chatUsageMonitor .model-row .label {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
#chatUsageMonitor .model-row .label .window-badge {
padding: 2px 6px;
border-radius: 8px;
font-size: 0.75em;
font-weight: 600;
color: #fff;
user-select: none;
}
#chatUsageMonitor .window-badge.hour3 {
background-color: ${COLORS.hour3};
}
#chatUsageMonitor .window-badge.daily {
background-color: ${COLORS.daily};
}
#chatUsageMonitor .window-badge.weekly {
background-color: ${COLORS.weekly};
}
#chatUsageMonitor .model-row .usage {
font-weight: 600;
font-variant-numeric: tabular-nums;
color: ${COLORS.success};
}
#chatUsageMonitor .toggle-btn {
background: none;
border: none;
color: ${COLORS.secondaryText};
cursor: pointer;
font-size: 18px;
line-height: 1;
padding: 4px 8px;
user-select: none;
transition: color 0.2s ease;
}
#chatUsageMonitor .toggle-btn:hover {
color: ${COLORS.primary};
}
#chatUsageMonitor svg {
fill: none; /*不使用填充,使用stroke*/
stroke: currentColor; /* 使用当前文本颜色 */
stroke-width: 2;
width: 18px;
height: 18px;
display: block;
}
/* 用量统计容器的动画 显示隐藏 */
#chatUsageMonitor .usage-content.hide {
display: block;
visibility: hidden;
opacity: 0;
height: 0;
overflow: hidden;
transition: visibility 0.3s, opacity 0.3s, height 0.3s;
}
#chatUsageMonitor .usage-content:not(.hide) {
visibility: visible;
opacity: 1;
height: auto;
}
`);
// ---辅助函数---
// 生成SVG图标 - 时钟
function createClockIcon() {
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("aria-hidden", "true");
svg.setAttribute("focusable", "false");
const circle = document.createElementNS(svgNS, "circle");
circle.setAttribute("cx", "12");
circle.setAttribute("cy", "12");
circle.setAttribute("r", "10");
circle.setAttribute("stroke", "currentColor"); // 使用currentColor
circle.setAttribute("fill", "none");
circle.setAttribute("stroke-width", "2");
const line1 = document.createElementNS(svgNS, "line");
line1.setAttribute("x1", "12");
line1.setAttribute("y1", "6");
line1.setAttribute("x2", "12");
line1.setAttribute("y2", "12");
line1.setAttribute("stroke", "currentColor"); // 使用currentColor
line1.setAttribute("stroke-width", "2");
line1.setAttribute("stroke-linecap", "round");
const line2 = document.createElementNS(svgNS, "line");
line2.setAttribute("x1", "12");
line2.setAttribute("y1", "12");
line2.setAttribute("x2", "16");
line2.setAttribute("y2", "14");
line2.setAttribute("stroke", "currentColor"); // 使用currentColor
line2.setAttribute("stroke-width", "2");
line2.setAttribute("stroke-linecap", "round");
svg.appendChild(circle);
svg.appendChild(line1);
svg.appendChild(line2);
return svg;
}
// 清理旧请求,保留对应时间窗口内的请求
function cleanupExpiredRequests() {
const now = Date.now();
const maxWindow = TIME_WINDOWS.weekly; //最长时间窗口
Object.values(usageData.models).forEach(model => {
model.requests = model.requests.filter(req => now - req.timestamp < maxWindow);
});
}
// --- 主UI逻辑 ---
let monitorDiv = null;
let usageContentDiv = null;
let minimized = true;
let isPinnedOpen = false; // 是否点击后持续展开
// 更新统计信息显示(刷新)
function updateUsageDisplay() {
if (!usageContentDiv) return;
usageContentDiv.innerHTML = ""; //清空内容
const now = Date.now();
// 按窗口类型排序顺序
const windowOrder = ["hour3", "daily", "weekly"];
// 从models中筛选有quota且可用的模型,并排序
let modelsList = Object.entries(usageData.models);
// 默认展示所有可用额度,故用的是quota,不过滤请求数为0的
// 这样不会遗漏配额为0的模型(无限额显示∞)
modelsList.sort(([k1, m1], [k2, m2]) => {
const aOrder = windowOrder.indexOf(m1.windowType);
const bOrder = windowOrder.indexOf(m2.windowType);
if (aOrder !== bOrder) return aOrder - bOrder;
// 同窗口类型内按key字母序
return k1.localeCompare(k2);
});
modelsList.forEach(([key, model]) => {
const windowMs = TIME_WINDOWS[model.windowType];
const count = model.requests.filter(req => now - req.timestamp < windowMs).length;
// 构造一行显示元素
const row = document.createElement("div");
row.className = "model-row";
// 左侧显示label和窗口badge
const labelSpan = document.createElement("span");
labelSpan.className = "label";
// 彩色圆点(取对应颜色)
const dot = document.createElement("span");
dot.style.display = "inline-block";
dot.style.width = "10px";
dot.style.height = "10px";
dot.style.borderRadius = "50%";
dot.style.backgroundColor = COLORS[model.windowType] || COLORS.gray;
dot.style.marginRight = "6px";
labelSpan.appendChild(dot);
// 模型名文本
const modelNameText = document.createElement("span");
modelNameText.textContent = key;
labelSpan.appendChild(modelNameText);
// 窗口类型badge
const windowBadge = document.createElement("span");
windowBadge.className = "window-badge " + model.windowType;
windowBadge.textContent = model.windowType === "hour3" ? "3h" : (model.windowType === "daily" ? "24h" : "7d");
labelSpan.appendChild(windowBadge);
row.appendChild(labelSpan);
// 右侧显示用量
const usageSpan = document.createElement("span");
usageSpan.className = "usage";
// quota为0 视为无限制
const quotaDisplay = model.quota > 0 ? model.quota : "∞";
// 默认显示 格式:已用 / 配额
usageSpan.textContent = `${count}/${quotaDisplay}`;
row.appendChild(usageSpan);
usageContentDiv.appendChild(row);
});
// 针对特殊模型 gpt-4o-mini
if (usageData.miniCount && usageData.miniCount > 0) {
const specialRow = document.createElement("div");
specialRow.className = "model-row";
const labelSpan = document.createElement("span");
labelSpan.className = "label";
const italicName = document.createElement("span");
italicName.textContent = SPECIAL_MODEL;
italicName.style.fontStyle = "italic";
italicName.style.color = COLORS.secondaryText;
labelSpan.appendChild(italicName);
specialRow.appendChild(labelSpan);
const usageSpan = document.createElement("span");
usageSpan.className = "usage";
usageSpan.textContent = `${usageData.miniCount} 次`;
usageSpan.style.fontStyle = "normal";
specialRow.appendChild(usageSpan);
usageContentDiv.appendChild(specialRow);
}
// 空状态提示(所有模型空)
if (usageContentDiv.childElementCount === 0) {
const emptyEl = document.createElement("div");
emptyEl.style.color = COLORS.secondaryText;
emptyEl.style.textAlign = "center";
emptyEl.style.fontStyle = "italic";
emptyEl.textContent = "使用模型后才会显示用量统计。";
usageContentDiv.appendChild(emptyEl);
}
}
// 创建监控器UI(主容器)
function createMonitorUI() {
if (monitorDiv) return; // 已创建直接返回
monitorDiv = document.createElement("div");
monitorDiv.id = "chatUsageMonitor";
monitorDiv.title = "用量统计";
// 初始化为折叠状态
minimized = true;
updateMinimizedState();
// 内容区域,显示用量统计详情
usageContentDiv = document.createElement("div");
usageContentDiv.className = "usage-content hide"; // 初始隐藏(折叠时隐藏)
monitorDiv.appendChild(usageContentDiv);
// 图标按钮(时钟)
const icon = createClockIcon();
monitorDiv.appendChild(icon);
// 事件 - 鼠标悬停显示内容,移出隐藏(非点击固定展开时)
monitorDiv.addEventListener("mouseenter", () => {
if (!isPinnedOpen) {
expandPanel();
}
});
monitorDiv.addEventListener("mouseleave", () => {
if (!isPinnedOpen) {
collapsePanel();
}
});
// 点击按钮切换固定展开/折叠
monitorDiv.addEventListener("click", (e) => {
// 如果拖拽中,忽略点击
if (dragging) return;
isPinnedOpen = !isPinnedOpen;
if (isPinnedOpen) {
expandPanel(true);
} else {
collapsePanel(true);
}
});
// 可拖拽
setupDraggable(monitorDiv);
document.body.appendChild(monitorDiv);
updateUsageDisplay();
}
// 更新折叠状态UI表现
function updateMinimizedState() {
if (!monitorDiv) return;
if (minimized) {
monitorDiv.classList.add("minimized");
if (usageContentDiv) usageContentDiv.classList.add("hide");
} else {
monitorDiv.classList.remove("minimized");
if (usageContentDiv) usageContentDiv.classList.remove("hide");
}
}
// 展开面板(显示详细信息)
function expandPanel(pin = false) {
minimized = false;
if (pin) isPinnedOpen = true;
updateMinimizedState();
updateUsageDisplay();
}
// 折叠面板(隐藏详细信息)
function collapsePanel(pin = false) {
minimized = true;
if (pin) isPinnedOpen = false;
updateMinimizedState();
}
// --- 拖拽支持 ---
// 拖拽时忽略点击防止误操作展开/折叠
let dragging = false;
function setupDraggable(element) {
let startX, startY, origX, origY;
element.style.position = "fixed";
element.style.top = usageData.position.y !== null ? usageData.position.y + "px" : null;
element.style.left = usageData.position.x !== null ? usageData.position.x + "px" : null;
element.style.right = "20px";
element.style.top = usageData.position.y !== null ? usageData.position.y + "px" : "20px";
element.addEventListener("mousedown", (e) => {
// 只允许左键拖拽
if (e.button !== 0) return;
// 禁止对input/select/button等控件拖拽
const tag = e.target.tagName.toLowerCase();
if (["input", "select", "button", "textarea"].includes(tag)) return;
dragging = false;
startX = e.clientX;
startY = e.clientY;
const rect = element.getBoundingClientRect();
origX = rect.left;
origY = rect.top;
function onMouseMove(ev) {
const deltaX = ev.clientX - startX;
const deltaY = ev.clientY - startY;
if (!dragging) {
if (Math.abs(deltaX) >= 5 || Math.abs(deltaY) >= 5) {
dragging = true;
} else {
return; //忽略小移动
}
}
let newX = origX + deltaX;
let newY = origY + deltaY;
// 限制边界防止跑出视口
newX = Math.min(Math.max(0, newX), window.innerWidth - rect.width);
newY = Math.min(Math.max(0, newY), window.innerHeight - rect.height);
element.style.left = newX + "px";
element.style.top = newY + "px";
element.style.right = "auto";
element.style.bottom = "auto";
}
function onMouseUp(ev) {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
if (dragging) {
dragging = false;
// 保存位置
const rect = element.getBoundingClientRect();
usageData.position = { x: rect.left, y: rect.top };
Storage.set(usageData);
}
}
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
e.preventDefault();
});
}
// --- 监听和刷新逻辑 ---
// 清理过期记录
function periodicCleanup() {
cleanupExpiredRequests();
Storage.set(usageData);
}
// 刷新UI显示
function periodicUpdate() {
usageData = Storage.get();
if (monitorDiv && !minimized) {
updateUsageDisplay();
}
}
// --- 用于脚本初始化 ---
function initialize() {
usageData = Storage.get();
cleanupExpiredRequests();
Storage.set(usageData);
createMonitorUI();
// 初始化时默认折叠,显示图标
minimized = true;
isPinnedOpen = false;
updateMinimizedState();
const posX = usageData.position.x;
const posY = usageData.position.y;
if (posX !== null && posY !== null) {
element.style.left = posX + "px";
element.style.top = posY + "px";
element.style.right = "auto";
element.style.bottom = "auto";
} else {
// 默认靠右上。但不要写right/top强制覆盖
element.style.top = "20px";
element.style.right = "20px";
}
// 周期刷新数据和UI
setInterval(() => {
periodicCleanup();
periodicUpdate();
}, 60 * 1000);
}
// --- 拦截fetch,监测模型使用 ---
const targetWindow = typeof unsafeWindow === "undefined" ? window : unsafeWindow;
if (!targetWindow._fetchWrappedUsageMonitor) {
const originalFetch = targetWindow.fetch;
targetWindow.fetch = new Proxy(originalFetch, {
apply: async (target, thisArg, args) => {
const response = await target.apply(thisArg, args);
try {
const [reqInfo, reqInit] = args;
const fetchUrl = typeof reqInfo === "string" ? reqInfo : reqInfo?.href;
if (reqInit?.method === "POST" && fetchUrl?.endsWith("/conversation")) {
const bodyText = reqInit.body;
if (bodyText) {
const bodyObj = JSON.parse(bodyText);
if (bodyObj?.model) {
const modelId = bodyObj.model;
// 特殊模型计数
if (modelId === SPECIAL_MODEL) {
usageData.miniCount = (usageData.miniCount || 0) + 1;
Storage.set(usageData);
updateUsageDisplay();
} else {
// 代理请求计数,保存时间戳
if (!usageData.models[modelId]) {
usageData.models[modelId] = {
requests: [],
quota: 50,
windowType: "daily",
};
}
// 清理旧请求,防止爆炸
cleanupExpiredRequests();
usageData.models[modelId].requests.push({ timestamp: Date.now() });
Storage.set(usageData);
updateUsageDisplay();
}
}
}
}
} catch (e) {
console.warn("[UsageMonitor] fetch hook failed:", e);
}
return response;
}
});
targetWindow._fetchWrappedUsageMonitor = true;
}
// DOM内容加载完成初始化
if (document.readyState === "loading") {
targetWindow.addEventListener("DOMContentLoaded", initialize);
} else {
setTimeout(initialize, 300);
}
// 页面路由切换重新初始化
window.addEventListener("popstate", () => setTimeout(initialize, 300));
// 观察DOM删除状态,若元素被删除,自动重建
const observer = new MutationObserver(() => {
if (!document.getElementById("chatUsageMonitor")) {
setTimeout(() => {
usageData = Storage.get();
createMonitorUI();
}, 300);
}
});
observer.observe(document.documentElement || document.body, {
childList: true,
subtree: true,
});
})();
谢谢!!
再调了一下,但是我希望的时钟图标还是没出来
更新了一下,已上传Greasyfork
非常nice,支持
感谢~~
更新了,支持导入、导出、合并记录!
太goood了