// ==UserScript==
// @name DeepSeek 开放平台 - 缓存命中率统计面板 (API 版)
// @namespace https://platform.deepseek.com
// @version 2.0.0
// @description 通过拦截 /api/v0/usage/amount 接口,自动计算当天和当月的缓存命中率、输入/输出 Token 数量,面板显示
// @author You
// @match https://platform.deepseek.com/*
// @icon https://platform.deepseek.com/favicon.ico
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ==================== 配置 ====================
const CONFIG = {
STORAGE_PREFIX: 'ds_cache_stats_v2_',
DATA_RETENTION_DAYS: 90,
REFRESH_INTERVAL: 5000,
DEBUG: false,
PANEL_DEFAULT_TOP: 120,
PANEL_DEFAULT_RIGHT: 20,
};
// ==================== 工具函数 ====================
const logger = {
log: (...args) => CONFIG.DEBUG && console.log('[DS-Cache-Stats]', ...args),
warn: (...args) => console.warn('[DS-Cache-Stats]', ...args),
error: (...args) => console.error('[DS-Cache-Stats]', ...args),
};
function getTodayStr() {
const d = new Date();
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-' +
String(d.getDate()).padStart(2, '0');
}
function getMonthFirstDayStr() {
const d = new Date();
return d.getFullYear() + '-' +
String(d.getMonth() + 1).padStart(2, '0') + '-01';
}
function isDateInCurrentMonth(dateStr) {
if (!dateStr) return false;
const now = new Date();
const target = new Date(dateStr);
return target.getFullYear() === now.getFullYear() &&
target.getMonth() === now.getMonth();
}
function formatNumber(num) {
if (num == null || isNaN(num)) return '0';
return Number(num).toLocaleString('en-US');
}
function formatPercent(rate) {
if (rate == null || isNaN(rate)) return '0.00%';
return rate.toFixed(2) + '%';
}
function safeGet(obj, path, defaultValue = null) {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current == null || typeof current !== 'object') return defaultValue;
current = current[key];
}
return current != null ? current : defaultValue;
}
function calcHitRate(hit, miss) {
const total = hit + miss;
if (total <= 0) return 0;
return (hit / total) * 100;
}
// ==================== 数据存储 ====================
const Storage = {
getAllData() {
try {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(CONFIG.STORAGE_PREFIX)) {
const dateStr = key.replace(CONFIG.STORAGE_PREFIX, '');
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
const value = localStorage.getItem(key);
if (value) {
try { data[dateStr] = JSON.parse(value); } catch (e) {}
}
}
}
}
return data;
} catch (e) {
logger.error('读取localStorage失败:', e);
return {};
}
},
saveDayData(dateStr, newData) {
if (!dateStr || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return;
try {
const key = CONFIG.STORAGE_PREFIX + dateStr;
const existing = localStorage.getItem(key);
let merged = newData;
if (existing) {
try {
const existingData = JSON.parse(existing);
merged = {
inputCacheHit: Math.max(safeGet(existingData, 'inputCacheHit', 0), safeGet(newData, 'inputCacheHit', 0)),
inputCacheMiss: Math.max(safeGet(existingData, 'inputCacheMiss', 0), safeGet(newData, 'inputCacheMiss', 0)),
outputTokens: Math.max(safeGet(existingData, 'outputTokens', 0), safeGet(newData, 'outputTokens', 0)),
lastUpdated: newData.lastUpdated || Date.now(),
};
} catch (e) { merged = newData; }
}
localStorage.setItem(key, JSON.stringify(merged));
logger.log('保存数据:', dateStr, merged);
} catch (e) {
logger.error('保存数据失败:', e);
}
},
cleanup() {
try {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - CONFIG.DATA_RETENTION_DAYS);
const cutoffStr = cutoff.toISOString().split('T')[0];
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(CONFIG.STORAGE_PREFIX)) {
const dateStr = key.replace(CONFIG.STORAGE_PREFIX, '');
if (dateStr < cutoffStr) keysToRemove.push(key);
}
}
keysToRemove.forEach(k => localStorage.removeItem(k));
if (keysToRemove.length > 0) logger.log('清理过期数据:', keysToRemove.length);
} catch (e) { logger.error('清理数据失败:', e); }
},
getTodaySummary() {
const allData = this.getAllData();
const todayData = allData[getTodayStr()] || {};
return {
inputCacheHit: safeGet(todayData, 'inputCacheHit', 0),
inputCacheMiss: safeGet(todayData, 'inputCacheMiss', 0),
outputTokens: safeGet(todayData, 'outputTokens', 0),
totalInput: (safeGet(todayData, 'inputCacheHit', 0) + safeGet(todayData, 'inputCacheMiss', 0)),
cacheHitRate: calcHitRate(
safeGet(todayData, 'inputCacheHit', 0),
safeGet(todayData, 'inputCacheMiss', 0)
),
};
},
getMonthSummary() {
const allData = this.getAllData();
let inputCacheHit = 0, inputCacheMiss = 0, outputTokens = 0;
for (const [dateStr, dayData] of Object.entries(allData)) {
if (isDateInCurrentMonth(dateStr)) {
inputCacheHit += safeGet(dayData, 'inputCacheHit', 0);
inputCacheMiss += safeGet(dayData, 'inputCacheMiss', 0);
outputTokens += safeGet(dayData, 'outputTokens', 0);
}
}
return {
inputCacheHit,
inputCacheMiss,
outputTokens,
totalInput: inputCacheHit + inputCacheMiss,
cacheHitRate: calcHitRate(inputCacheHit, inputCacheMiss),
};
},
};
// ==================== API 拦截与数据解析 ====================
function parseApiResponse(responseData) {
const days = safeGet(responseData, 'data.biz_data.days', []);
if (!Array.isArray(days) || days.length === 0) return;
days.forEach(day => {
const dateStr = day.date;
let hit = 0, miss = 0, output = 0;
if (Array.isArray(day.data)) {
day.data.forEach(modelUsage => {
if (Array.isArray(modelUsage.usage)) {
modelUsage.usage.forEach(u => {
const amount = Number(u.amount || 0);
switch (u.type) {
case 'PROMPT_CACHE_HIT_TOKEN':
hit += amount;
break;
case 'PROMPT_CACHE_MISS_TOKEN':
miss += amount;
break;
case 'RESPONSE_TOKEN':
output += amount;
break;
}
});
}
});
}
if (hit > 0 || miss > 0 || output > 0) {
Storage.saveDayData(dateStr, {
inputCacheHit: hit,
inputCacheMiss: miss,
outputTokens: output,
lastUpdated: Date.now(),
});
}
});
logger.log('API 数据已解析并存储,共', days.length, '天');
updatePanel();
}
function interceptFetch() {
const originalFetch = window.fetch;
const self = this;
window.fetch = async function (...args) {
const response = await originalFetch.apply(this, args);
try {
const url = args[0] instanceof Request ? args[0].url : args[0];
if (url.includes('/api/v0/usage/amount')) {
const cloned = response.clone();
cloned.json().then(data => {
parseApiResponse(data);
}).catch(() => {});
}
} catch (e) {}
return response;
};
}
function interceptXHR() {
const OriginalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function () {
const xhr = new OriginalXHR();
let requestURL = '';
const originalOpen = xhr.open;
xhr.open = function (method, url, ...rest) {
requestURL = url;
return originalOpen.apply(this, [method, url, ...rest]);
};
const originalSend = xhr.send;
xhr.send = function (...args) {
xhr.addEventListener('load', function () {
if (requestURL.includes('/api/v0/usage/amount') && xhr.responseText) {
try {
const data = JSON.parse(xhr.responseText);
parseApiResponse(data);
} catch (e) {}
}
});
return originalSend.apply(this, args);
};
return xhr;
};
// 保持静态属性
window.XMLHttpRequest.prototype = OriginalXHR.prototype;
for (const key in OriginalXHR) {
if (OriginalXHR.hasOwnProperty(key)) {
window.XMLHttpRequest[key] = OriginalXHR[key];
}
}
}
// ==================== UI 面板 ====================
let panelElement = null;
let panelMinimized = false;
let isDragging = false;
let dragStartX, dragStartY, panelStartX, panelStartY;
function createPanel() {
if (panelElement && document.body.contains(panelElement)) return;
if (panelElement) panelElement.remove();
const panel = document.createElement('div');
panel.id = 'ds-cache-stats-panel';
panel.innerHTML = `
📅 今天
✅ 缓存命中--tokens
❌ 缓存未命中--tokens
📤 输出--tokens
🎯 命中率--
📆 本月累计
✅ 缓存命中--tokens
❌ 缓存未命中--tokens
📤 输出--tokens
🎯 命中率--
`;
panel.style.top = CONFIG.PANEL_DEFAULT_TOP + 'px';
panel.style.right = CONFIG.PANEL_DEFAULT_RIGHT + 'px';
document.body.appendChild(panel);
panelElement = panel;
bindPanelEvents();
}
function bindPanelEvents() {
if (!panelElement) return;
const header = panelElement.querySelector('#ds-stats-header');
const minBtn = panelElement.querySelector('#ds-stats-min-btn');
const closeBtn = panelElement.querySelector('#ds-stats-close-btn');
const refreshBtn = panelElement.querySelector('#ds-stats-refresh-btn');
const body = panelElement.querySelector('#ds-stats-body');
header?.addEventListener('mousedown', onDragStart);
header?.addEventListener('touchstart', onDragStart, { passive: false });
minBtn?.addEventListener('click', (e) => {
e.stopPropagation();
panelMinimized = !panelMinimized;
if (body) body.style.display = panelMinimized ? 'none' : 'block';
minBtn.textContent = panelMinimized ? '□' : '─';
if (panelMinimized) panelElement.style.height = 'auto';
else panelElement.style.height = '';
});
closeBtn?.addEventListener('click', (e) => {
e.stopPropagation();
panelElement.style.display = 'none';
setTimeout(() => { if (panelElement) panelElement.style.display = ''; }, 180000);
});
refreshBtn?.addEventListener('click', () => {
updatePanel();
refreshBtn.style.transform = 'rotate(360deg)';
setTimeout(() => { refreshBtn.style.transform = ''; }, 600);
});
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
document.addEventListener('touchmove', onDragMove, { passive: false });
document.addEventListener('touchend', onDragEnd);
}
function onDragStart(e) {
if (e.target.closest('button')) return;
isDragging = true;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
dragStartX = clientX;
dragStartY = clientY;
panelStartX = panelElement.offsetLeft;
panelStartY = panelElement.offsetTop;
panelElement.style.transition = 'none';
panelElement.style.cursor = 'grabbing';
e.preventDefault();
}
function onDragMove(e) {
if (!isDragging) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const dx = clientX - dragStartX;
const dy = clientY - dragStartY;
let newRight = panelStartX - dx;
let newTop = panelStartY + dy;
const maxRight = window.innerWidth - 100;
const minRight = -panelElement.offsetWidth + 40;
newRight = Math.max(minRight, Math.min(newRight, maxRight));
newTop = Math.max(0, Math.min(newTop, window.innerHeight - 60));
panelElement.style.right = newRight + 'px';
panelElement.style.top = newTop + 'px';
panelElement.style.left = 'auto';
e.preventDefault();
}
function onDragEnd() {
if (isDragging) {
isDragging = false;
if (panelElement) {
panelElement.style.transition = '';
panelElement.style.cursor = '';
}
}
}
function updatePanel() {
if (!panelElement || !document.body.contains(panelElement)) createPanel();
const todaySummary = Storage.getTodaySummary();
const monthSummary = Storage.getMonthSummary();
const todayDateEl = panelElement.querySelector('#ds-today-date');
const monthRangeEl = panelElement.querySelector('#ds-month-range');
if (todayDateEl) todayDateEl.textContent = `(${getTodayStr()})`;
if (monthRangeEl) monthRangeEl.textContent = `(${getMonthFirstDayStr()} ~ ${getTodayStr()})`;
const updateEl = (id, text) => {
const el = panelElement?.querySelector('#' + id);
if (el) el.textContent = text;
};
updateEl('ds-today-hit', formatNumber(todaySummary.inputCacheHit));
updateEl('ds-today-miss', formatNumber(todaySummary.inputCacheMiss));
updateEl('ds-today-output', formatNumber(todaySummary.outputTokens));
updateEl('ds-today-rate', formatPercent(todaySummary.cacheHitRate));
updateRateColor('ds-today-rate', todaySummary.cacheHitRate);
updateEl('ds-month-hit', formatNumber(monthSummary.inputCacheHit));
updateEl('ds-month-miss', formatNumber(monthSummary.inputCacheMiss));
updateEl('ds-month-output', formatNumber(monthSummary.outputTokens));
updateEl('ds-month-rate', formatPercent(monthSummary.cacheHitRate));
updateRateColor('ds-month-rate', monthSummary.cacheHitRate);
const updatedEl = panelElement.querySelector('#ds-stats-updated');
if (updatedEl) updatedEl.textContent = '更新于 ' + new Date().toLocaleTimeString('zh-CN');
}
function updateRateColor(id, rate) {
const el = panelElement?.querySelector('#' + id);
if (!el) return;
if (rate >= 70) el.style.color = '#10b981';
else if (rate >= 30) el.style.color = '#f59e0b';
else if (rate > 0) el.style.color = '#ef4444';
else el.style.color = '#94a3b8';
}
// ==================== 样式注入 ====================
function injectStyles() {
const style = document.createElement('style');
style.id = 'ds-cache-stats-styles';
style.textContent = `
#ds-cache-stats-panel {
position: fixed;
z-index: 99999;
width: 340px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.08);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
font-size: 13px;
color: #1e293b;
border: 1px solid #e2e8f0;
transition: box-shadow 0.3s ease;
user-select: none;
overflow: hidden;
animation: ds-panel-fade-in 0.3s ease;
}
@keyframes ds-panel-fade-in {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
#ds-cache-stats-panel:hover { box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.1); }
.ds-stats-header {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 14px; background: linear-gradient(135deg, #4F6EF7 0%, #3b5de7 100%);
color: #ffffff; cursor: move; border-radius: 12px 12px 0 0;
font-weight: 600; font-size: 14px; letter-spacing: 0.3px;
}
.ds-stats-title { display: flex; align-items: center; gap: 6px; }
.ds-stats-header-btns { display: flex; gap: 4px; }
.ds-stats-btn {
background: rgba(255,255,255,0.2); border: none; color: #fff;
cursor: pointer; padding: 3px 8px; border-radius: 4px; font-size: 14px;
line-height: 1; transition: background 0.2s;
}
.ds-stats-btn:hover { background: rgba(255,255,255,0.35); }
.ds-stats-close-btn:hover { background: rgba(255,80,80,0.5); }
.ds-stats-body { padding: 10px 14px; max-height: 500px; overflow-y: auto; }
.ds-stats-section { margin-bottom: 4px; }
.ds-stats-section-title {
font-weight: 600; font-size: 13px; color: #475569;
margin-bottom: 8px; padding-bottom: 4px; border-bottom: 2px solid #e2e8f0;
}
.ds-stats-section-title span { font-weight: 400; font-size: 11px; color: #94a3b8; }
.ds-stats-row { display: flex; align-items: center; padding: 5px 0; justify-content: space-between; }
.ds-stats-highlight { background: #f8fafc; border-radius: 6px; padding: 7px 8px; margin-top: 4px; font-weight: 600; }
.ds-stats-label { flex: 1; font-size: 12px; color: #64748b; }
.ds-label-hit { color: #10b981; } .ds-label-miss { color: #f59e0b; } .ds-label-output { color: #6366f1; }
.ds-stats-value { flex: 0 0 auto; text-align: right; font-weight: 600; font-size: 13px; color: #1e293b; min-width: 80px; padding-right: 4px; }
.ds-stats-rate { font-size: 16px; font-weight: 700; }
.ds-stats-unit { flex: 0 0 auto; font-size: 10px; color: #94a3b8; width: 36px; text-align: left; }
.ds-stats-divider { height: 1px; background: #e2e8f0; margin: 8px 0; }
.ds-stats-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; padding-top: 6px; border-top: 1px solid #f1f5f9; }
.ds-stats-updated { font-size: 10px; color: #94a3b8; }
.ds-stats-refresh-btn {
background: #f1f5f9; border: 1px solid #e2e8f0; padding: 4px 10px;
border-radius: 6px; cursor: pointer; font-size: 11px; color: #475569;
transition: all 0.3s ease;
}
.ds-stats-refresh-btn:hover { background: #e2e8f0; border-color: #cbd5e1; }
@media (max-width: 480px) {
#ds-cache-stats-panel { width: 280px; font-size: 11px; right: 4px !important; }
.ds-stats-value { font-size: 11px; min-width: 60px; }
.ds-stats-rate { font-size: 14px; }
.ds-stats-header { padding: 8px 10px; font-size: 12px; }
.ds-stats-body { padding: 8px 10px; }
}
`;
document.head.appendChild(style);
}
// ==================== 初始化 ====================
function init() {
logger.log('启动 API 拦截版缓存命中率统计');
injectStyles();
Storage.cleanup();
createPanel();
interceptFetch();
interceptXHR();
updatePanel();
setInterval(updatePanel, CONFIG.REFRESH_INTERVAL);
setInterval(Storage.cleanup, 3600000);
// 页面切换时确保面板存在
const observer = new MutationObserver(() => {
if (panelElement && !document.body.contains(panelElement)) {
createPanel();
updatePanel();
}
});
observer.observe(document.body, { childList: true, subtree: true });
logger.log('初始化完成。访问用量统计页面即可自动捕获数据。');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 500));
} else {
setTimeout(init, 500);
}
})();