// ==UserScript== // @name DeepSeek 开放平台 - 缓存命中率统计面板 (多 Key 版) // @namespace https://platform.deepseek.com // @version 3.0.0 // @description 通过拦截 /api/v0/usage/amount 接口,自动识别不同 API Key (group_id),支持切换查看当天/本月缓存命中率、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_v3_', 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; } // ==================== 数据存储(按 groupId 分组) ==================== const Storage = { // 构造存储键名:ds_cache_stats_v3_${groupId}_${dateStr} _makeKey(groupId, dateStr) { return CONFIG.STORAGE_PREFIX + groupId + '_' + dateStr; }, // 获取所有数据,返回 { groupId: { dateStr: {...} } } getAllData() { try { const data = {}; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(CONFIG.STORAGE_PREFIX)) { // 移除前缀,剩下的格式为 groupId_dateStr const rest = key.substring(CONFIG.STORAGE_PREFIX.length); const lastUnderscoreIndex = rest.lastIndexOf('_'); if (lastUnderscoreIndex > 0) { const dateStr = rest.substring(lastUnderscoreIndex + 1); const groupId = rest.substring(0, lastUnderscoreIndex); if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) { const value = localStorage.getItem(key); if (value) { try { if (!data[groupId]) data[groupId] = {}; data[groupId][dateStr] = JSON.parse(value); } catch (e) { /* ignore parse errors */ } } } } } } return data; } catch (e) { logger.error('读取localStorage失败:', e); return {}; } }, saveDayData(groupId, dateStr, newData) { if (!groupId || !dateStr || !/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return; try { const key = this._makeKey(groupId, 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(`保存数据 [${groupId}] ${dateStr}:`, merged); } catch (e) { logger.error('保存数据失败:', e); } }, // 获取所有已记录的 groupId getAllGroupIds() { const data = this.getAllData(); return Object.keys(data); }, getTodaySummary(groupId) { const allData = this.getAllData(); const groupData = allData[groupId] || {}; const todayData = groupData[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(groupId) { const allData = this.getAllData(); const groupData = allData[groupId] || {}; let inputCacheHit = 0, inputCacheMiss = 0, outputTokens = 0; for (const [dateStr, dayData] of Object.entries(groupData)) { 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), }; }, // 清理过期数据(所有 group) 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 rest = key.substring(CONFIG.STORAGE_PREFIX.length); const lastUnderscoreIndex = rest.lastIndexOf('_'); if (lastUnderscoreIndex > 0) { const dateStr = rest.substring(lastUnderscoreIndex + 1); 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); } }, }; // ==================== API 拦截与数据解析 ==================== function parseApiResponse(responseData, groupId = 'default') { 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(groupId, dateStr, { inputCacheHit: hit, inputCacheMiss: miss, outputTokens: output, lastUpdated: Date.now(), }); } }); logger.log(`API 数据已解析 [${groupId}],共 ${days.length} 天`); updatePanel(); } function interceptFetch() { const originalFetch = window.fetch; 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')) { // 提取 group_id 或 api_key_id 参数 let groupId = 'default'; try { const urlObj = new URL(url, location.origin); groupId = urlObj.searchParams.get('group_id') || urlObj.searchParams.get('api_key_id') || 'default'; } catch (e) { /* 使用默认值 */ } const cloned = response.clone(); cloned.json().then(data => { parseApiResponse(data, groupId); }).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 { // 提取 group_id let groupId = 'default'; try { const urlObj = new URL(requestURL, location.origin); groupId = urlObj.searchParams.get('group_id') || urlObj.searchParams.get('api_key_id') || 'default'; } catch (e) {} const data = JSON.parse(xhr.responseText); parseApiResponse(data, groupId); } 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(); populateGroupSelect(); } function populateGroupSelect() { const select = document.getElementById('ds-group-select'); if (!select) return; const groupIds = Storage.getAllGroupIds(); const currentVal = select.value; // 如果没有已知 group 且当前有选中值,保留;否则添加占位选项 select.innerHTML = ''; if (groupIds.length === 0) { select.innerHTML = ''; return; } groupIds.forEach(id => { const option = document.createElement('option'); option.value = id; option.textContent = id; // 可以后续映射为友好名称 select.appendChild(option); }); if (groupIds.includes(currentVal)) { select.value = currentVal; } else { select.value = groupIds[0]; } } 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'); const groupSelect = panelElement.querySelector('#ds-group-select'); 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'; // 3 分钟后自动恢复 setTimeout(() => { if (panelElement) panelElement.style.display = ''; }, 180000); }); refreshBtn?.addEventListener('click', () => { updatePanel(); refreshBtn.style.transform = 'rotate(360deg)'; setTimeout(() => { refreshBtn.style.transform = ''; }, 600); }); groupSelect?.addEventListener('change', () => { updatePanel(); }); 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') || e.target.closest('select')) 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(); // 更新下拉框选项 populateGroupSelect(); const select = document.getElementById('ds-group-select'); const groupId = select?.value; if (!groupId) { // 没有数据时显示空状态 setEmptyValues(); return; } const todaySummary = Storage.getTodaySummary(groupId); const monthSummary = Storage.getMonthSummary(groupId); 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 setEmptyValues() { const ids = ['ds-today-hit', 'ds-today-miss', 'ds-today-output', 'ds-today-rate', 'ds-month-hit', 'ds-month-miss', 'ds-month-output', 'ds-month-rate']; ids.forEach(id => { const el = panelElement?.querySelector('#' + id); if (el) el.textContent = '--'; }); const updatedEl = panelElement?.querySelector('#ds-stats-updated'); if (updatedEl) updatedEl.textContent = '暂无数据'; } 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: 360px; 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; gap: 8px; } .ds-stats-title { white-space: nowrap; } .ds-group-select { padding: 2px 8px; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; background: rgba(255,255,255,0.15); color: #fff; font-size: 12px; max-width: 120px; cursor: pointer; outline: none; } .ds-group-select option { background: #fff; color: #1e293b; } .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: 300px; 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; } .ds-group-select { max-width: 90px; font-size: 10px; } } `; document.head.appendChild(style); } // ==================== 初始化 ==================== function init() { logger.log('启动多 Key 缓存命中率统计'); 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('初始化完成。访问用量统计页面并切换 Key 即可自动捕获。'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(init, 500)); } else { setTimeout(init, 500); } })();