// ==UserScript==
// @name ChatGPT请求计数器
// @namespace http://tampermonkey.net/
// @version 0.7
// @description 记录ChatGPT各个模型的调用情况
// @author muzi
// @icon https://chat.openai.com/favicon.ico
// @match https://chat.openai.com/
// @match https://chat.openai.com/?model=*
// @match https://chat.openai.com/c/*
// @match https://chat.openai.com/g/*
// @match https://chat.openai.com/gpts/*
// @match https://chatgpt.com/
// @match https://chatgpt.com/?model=*
// @match https://chatgpt.com/c/*
// @match https://chatgpt.com/g/*
// @match https://chatgpt.com/gpts
// @match https://chatgpt.com/gpts/*
// @match https://chatgpt.com/share/*
// @match https://chatgpt.com/share/*/continue
// @grant none
// @run-at document-end
// @license MIT
// @downloadURL https://update.greasyfork.icu/scripts/494383/ChatGPT%E8%AF%B7%E6%B1%82%E8%AE%A1%E6%95%B0%E5%99%A8.user.js
// @updateURL https://update.greasyfork.icu/scripts/494383/ChatGPT%E8%AF%B7%E6%B1%82%E8%AE%A1%E6%95%B0%E5%99%A8.meta.js
// ==/UserScript==
/*
1. 监听 fetch 函数调用
当 request_url=https://chat.openai.com/backend-api/conversation && request_method=POST 时,解析 post body 并记录其中的 model 字段
a. 从 localStorage 中读取 model_request_mapping,如果没有则创建一个空的 model_mapping
b. 写入数据 key 为 model,value 为每次调用的时间戳列表,存储到 localStorage 中
2. 记录模型调用次数
- 当天所有模型调用次数
- 当天 GPT-4 调用次数
- 剩余 GPT-4 调用次数(比如每3小时50条)
3. 支持导出数据
- 以 json 格式导出 model_request_mapping 数据
4. 支持自定义配置
a. 缓存时间,超过缓存时间的数据自动清理,避免 localStorage 过大
5. 支持记录模型生成的 token 数量
*/
/*
2024-04-16 ChangeLog
增加对 auto 模式的统计
*/
(function () {
"use strict";
const LIMIT_HOURS = 3; // 自定义配置:缓存时间
const GPT4_LIMIT = 40; // 自定义配置:GPT-4 调用限制
const panelHTML = `
`;
var originalFetch = window.fetch;
console.info("ChatGPTRequestCounter loaded");
window.fetch = function (url, options) {
// 重构:将条件逻辑提取到单独的函数中
processRequest(url, options);
// 调用原始的fetch函数
return originalFetch.apply(this, arguments);
};
function processRequest(url, options) {
if (
url.includes("/backend-api/conversation") &&
options &&
options.method === "POST"
) {
logRequest(options);
} else if (
url.includes("discovery.json") ||
url.includes("gizmos/g-") ||
url.includes("_next/data") ||
url.includes("backend-api/conversation")
) {
safeUpdateCount();
} else if (
url.endsWith("lat/r") &&
options &&
options.method === "POST"
) {
logTokenNum(options);
}
}
function logRequest(options) {
processData(options, "model_request_mapping");
}
function logTokenNum(options) {
processData(options, "tokenStatistics", (data) => ({
time: Date.now(),
response_tokens: data.count_tokens,
}));
}
function processData(
options,
storageKey,
dataMappingFn = (data) => Date.now()
) {
try {
const requestData = JSON.parse(options.body);
if (requestData.model) {
let storageData = JSON.parse(
localStorage.getItem(storageKey) || "{}"
);
if (!storageData[requestData.model]) {
storageData[requestData.model] = [];
}
storageData[requestData.model].push(dataMappingFn(requestData));
localStorage.setItem(storageKey, JSON.stringify(storageData));
safeUpdateCount();
}
} catch (err) {
console.error("Error processing request:", err);
}
}
function safeUpdateCount() {
try {
updateCount();
} catch (err) {
console.error("Error logging ChatGPT request:", err);
}
}
function logDailyTokenCount() {
const today = new Date().toDateString(); // 获取当天日期字符串
let tokenStatistics = JSON.parse(
localStorage.getItem("tokenStatistics") || "{}"
);
let dailyTokenCount = {};
for (let model in tokenStatistics) {
dailyTokenCount[model] = tokenStatistics[model]
.filter(
(entry) => new Date(entry.time).toDateString() === today
) // 筛选当天的数据
.reduce((total, entry) => total + entry.response_tokens, 0); // 累加token数量
}
return dailyTokenCount;
}
function countRequests() {
let gpt4_recent = 0;
let gpt4_today = 0;
let all_models_today = 0;
let dynamic_today = 0;
let dynamic_recent = 0;
const cutoffTime = Date.now() - LIMIT_HOURS * 60 * 60 * 1000;
const todayCutOffTime = new Date().setHours(0, 0, 0, 0);
const originalMapping = JSON.parse(
localStorage.getItem("model_request_mapping") || "{}"
);
const model_request_mapping = JSON.parse(
JSON.stringify(originalMapping)
); // deep copy
for (let model in model_request_mapping) {
const todayRequests = model_request_mapping[model].filter(
(timestamp) => timestamp > todayCutOffTime
);
all_models_today += todayRequests.length;
if (model.startsWith("gpt-4")) {
gpt4_today += todayRequests.length;
const recentRequests = todayRequests.filter(
(timestamp) => timestamp > cutoffTime
);
gpt4_recent += recentRequests.length;
}
// 新增处理 auto 开头的模型
if (model.startsWith("auto")) {
dynamic_today += todayRequests.length;
const recentRequests = todayRequests.filter(
(timestamp) => timestamp > cutoffTime
);
dynamic_recent += recentRequests.length;
}
}
return {
all_models_today,
gpt4_today,
gpt4_recent,
dynamic_today, // 新增
dynamic_recent, // 新增
};
}
// 插入展示面板
function insertPanel(panelHTML, exportData) {
// 避免重复插入
if (document.querySelector("#chatgpt-counter-panel")) {
console.log("Panel already inserted.");
return;
}
// 目标选择器
const targetSelector =
".flex.flex-col.pt-2.empty\\:hidden.dark\\:border-white\\/20";
console.log("Looking for profileDiv at:", targetSelector);
// 使用 MutationObserver 观察 DOM 变化
const observer = new MutationObserver((mutations, obs) => {
const profileDiv = document.querySelector(targetSelector);
if (profileDiv) {
// 避免重复插入
if (document.querySelector("#chatgpt-counter-panel")) {
console.log("Panel already inserted.");
obs.disconnect(); // 停止观察
return;
}
// 插入面板的 HTML
console.log("Inserting panel at:", targetSelector);
profileDiv.insertAdjacentHTML("afterbegin", panelHTML);
// 添加导出按钮的事件监听器
const exportButton = document.getElementById(
"chatgpt-counter-panel-export"
);
if (exportButton) {
exportButton.removeEventListener("click", exportData);
exportButton.addEventListener("click", exportData);
}
// 停止观察
obs.disconnect();
// 延迟更新计数,确保面板已插入
setTimeout(updateCount, 500);
} else {
console.warn(
"Target profileDiv not found. Waiting for further mutations..."
);
}
});
// 观察整个文档的子节点变化
observer.observe(document, {
childList: true,
subtree: true,
});
}
// 更新展示面板数值
function updateCount() {
const counts = countRequests();
const all_models_today = counts.all_models_today;
const gpt4_today = counts.gpt4_today;
const dynamic_today = counts.dynamic_today;
const remaining =
GPT4_LIMIT - counts.gpt4_recent - counts.dynamic_recent;
// 如果面板不存在,插入面板
insertPanel(panelHTML, exportData);
// 安全地更新元素的文本内容
const updateTextContent = (id, text) => {
const element = document.getElementById(id);
if (element) {
element.textContent = text;
} else {
console.error(`Element with ID '${id}' not found. waiting...`);
}
};
updateTextContent("total", all_models_today);
updateTextContent("gpt-4", gpt4_today);
updateTextContent("dynamic", dynamic_today);
updateTextContent("remain", remaining);
}
// 创建导出功能
function exportData() {
// 获取当天的Token计数
const dailyTokenCount = logDailyTokenCount();
const tokenCountMessage =
"今日模型输出统计:\n" + JSON.stringify(dailyTokenCount, null, 2);
// 询问用户是否下载请求记录,同时展示Token计数
const userConfirmed = confirm(
tokenCountMessage + "\n\n是否下载请求记录?"
);
if (userConfirmed) {
// 用户选择“确定”,触发下载
const dataStr =
"data:text/json;charset=utf-8," +
encodeURIComponent(
localStorage.getItem("model_request_mapping")
);
const downloadAnchorNode = document.createElement("a");
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute(
"download",
"model_request_mapping.json"
);
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
}
// every 5 minutes update count
setInterval(() => {
updateCount();
}, 5 * 60 * 1000);
updateCount();
})();