本地统计 ChatGPT 各模型总量 + 当日增量(按本地时区归零)
// ==UserScript==
// @name ChatGPT Model Usage Tracker (ultimate+DR) v3.5.1
// @namespace https://example.local
// @version 3.5.1
// @description 本地统计 ChatGPT 各模型总量 + 当日增量(按本地时区归零)
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @run-at document-start
// @grant unsafeWindow
// ==/UserScript==
(() => {
/******************** 0. 配置 ************************/
const KEY_TOTAL = '__cgpt_usage__';
const KEY_DAILY = '__cgpt_usage_daily__';
const KEY_DAYFLAG = '__cgpt_usage_day__';
const seenDR = new Set();
const seenMessage = new Set();
/******************** 1. 数据层 **********************/
const stats = JSON.parse(localStorage.getItem(KEY_TOTAL) || '{}');
let daily = JSON.parse(localStorage.getItem(KEY_DAILY) || '{}');
let dayFlag = localStorage.getItem(KEY_DAYFLAG) || '';
// **本地时区日期**
const todayStr = () => new Date().toLocaleDateString('en-CA'); // YYYY-MM-DD
const ensureToday = () => {
const now = todayStr();
if (now !== dayFlag) {
dayFlag = now;
daily = {};
localStorage.setItem(KEY_DAILY, JSON.stringify(daily));
localStorage.setItem(KEY_DAYFLAG, dayFlag);
}
};
const bump = (model) => {
ensureToday();
stats[model] = (stats[model] || 0) + 1;
daily[model] = (daily[model] || 0) + 1;
localStorage.setItem(KEY_TOTAL, JSON.stringify(stats));
localStorage.setItem(KEY_DAILY, JSON.stringify(daily));
render();
};
/******************** 2. UI *************************/
let box;
const mountUI = () => {
if (!document.body || (box && document.body.contains(box))) return;
box = document.createElement('div');
Object.assign(box.style, {
position:'fixed', right:'16px', bottom:'16px', zIndex:99999,
background:'rgba(0,0,0,.78)', color:'#fff', font:'13px/1.4 monospace',
padding:'8px 14px', borderRadius:'8px', whiteSpace:'pre-wrap',
userSelect:'none', cursor:'pointer', maxWidth:'300px'
});
box.onclick = () => {
if (confirm('清空统计数据?')) {
for (const k in stats) delete stats[k];
for (const k in daily) delete daily[k];
localStorage.setItem(KEY_TOTAL,'{}');
localStorage.setItem(KEY_DAILY,'{}');
render();
}
};
document.body.appendChild(box);
render();
};
const render = () => {
if (!box) return;
ensureToday();
const arrow = '▲';
const green = (txt) => `<span style="color:#4caf50">${txt}</span>`;
const lines = Object.keys(stats).length
? Object.entries(stats)
.sort((a,b)=>b[1]-a[1])
.map(([m,c])=>{
const inc = daily[m] ? ` ${green(arrow + '(+' + daily[m] + ')')}` : '';
return `${m}: ${c}${inc}`;
})
: ['暂无数据'];
box.innerHTML = '📊 模型统计<br>' + lines.join('<br>');
};
document.addEventListener('DOMContentLoaded', mountUI);
setInterval(mountUI, 3000);
/******************** 3. fetch/XHR 拦截(与 v3.5 相同) ********************/
const origFetch = unsafeWindow.fetch;
unsafeWindow.fetch = async function(input, init = {}) {
const req = input instanceof Request ? input : new Request(input, init);
if (req.method !== 'POST' || req.url.startsWith('chrome-extension')) {
return origFetch.apply(this, arguments);
}
let bodyStr = '';
try { bodyStr = await req.clone().text(); } catch(_) {}
hook(bodyStr);
return origFetch.apply(this, arguments);
};
(function() {
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
const urlCache = new WeakMap();
XMLHttpRequest.prototype.open = function(m,u,...rest) {
urlCache.set(this, {method:m,url:u});
return origOpen.call(this, m, u, ...rest);
};
XMLHttpRequest.prototype.send = function(body) {
const {method} = urlCache.get(this) || {};
if (method?.toUpperCase() === 'POST') {
readBody(body).then(hook);
}
return origSend.call(this, body);
};
})();
/******************** 5. 主逻辑(保持不变) **********************/
async function hook(bodyStr){
if (!bodyStr) return;
let payload;
try { payload = JSON.parse(bodyStr); } catch { return; }
if (Array.isArray(payload.system_hints) &&
payload.system_hints.includes('research')) {
// >>> 新增:仅统计真正由用户发起的深度研究请求 <<<
const firstMsg = payload.messages?.[0];
const role = firstMsg?.author?.role ?? firstMsg?.role;
if (role !== 'user') return; // 后台请求,直接忽略
const cid = payload.conversation_id;
if (cid && !seenDR.has(cid)) {
bump('deep_research');
seenDR.add(cid);
}
return;
}
if (!payload.model) return;
const firstMsg = payload.messages?.[0];
if (!firstMsg) return;
const role = firstMsg.author?.role ?? firstMsg.role;
const parts = firstMsg.content?.parts;
if (role !== 'user') return;
if (!Array.isArray(parts) || parts.length === 0) return;
const msgId = firstMsg.id;
if (msgId && seenMessage.has(msgId)) return;
if (msgId) seenMessage.add(msgId);
bump(payload.model);
}
async function readBody(b){
if (!b) return '';
if (typeof b === 'string') return b;
if (b instanceof FormData || b instanceof URLSearchParams) return b.toString();
if (b?.clone) {
try { return await b.clone().text(); } catch {}
}
return '';
}
console.log('[USAGE-TRACKER] v3.5.1 injected');
})();