// ==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); } })();