// ==UserScript== // @name DeepSeek 用量增强统计 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 在 DeepSeek 用量页面增加高级分析图表:每日费用趋势、缓存命中率、Token分布、Key对比等 // @author opencode // @match https://platform.deepseek.com/usage* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ============ 配置 ============ const STATS_TZ_OFFSET_SECONDS = 28800; const MODEL_COLORS = ['#6366f1', '#f59e0b', '#10b981', '#ef4444', '#8b5cf6', '#06b6d4', '#f97316', '#ec4899']; const MODEL_NAMES_CN = { 'deepseek-v4-flash': 'V4-Flash', 'deepseek-v4-pro': 'V4-Pro', 'deepseek-chat & deepseek-reasoner': 'Chat & Reasoner', 'deepseek-chat': 'Chat', 'deepseek-reasoner': 'Reasoner', }; const MODEL_RATES = { 'deepseek-v4-flash': { input: 2, cache_hit: 0.2, output: 8 }, 'deepseek-v4-pro': { input: 4, cache_hit: 0.4, output: 16 }, 'deepseek-chat & deepseek-reasoner': { input: 2, cache_hit: 0.2, output: 8 }, }; // ============ 主题 ============ let isDark = true; function detectTheme() { const pref = localStorage.getItem('__appKit_@deepseek/platform_themePreference'); if (pref === 'light') return false; if (pref === 'dark') return true; return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? true; } isDark = detectTheme(); function theme() { return isDark ? { bg: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)', cardBg: 'rgba(255,255,255,0.04)', cardBorder: 'rgba(255,255,255,0.06)', modelBg: 'rgba(255,255,255,0.03)', modelBorder: 'rgba(255,255,255,0.06)', text: '#e0e0e0', textDim: '#888', textFaint: '#666', titleColor: '#fff', shadow: '0 4px 20px rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.08)', barText: '#aaa', barDate: '#666', chartBorder: 'rgba(255,255,255,0.08)', } : { bg: 'linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%)', cardBg: 'rgba(0,0,0,0.06)', cardBorder: 'rgba(0,0,0,0.08)', modelBg: 'rgba(0,0,0,0.04)', modelBorder: 'rgba(0,0,0,0.1)', text: '#1e293b', textDim: '#475569', textFaint: '#94a3b8', titleColor: '#0f172a', shadow: '0 4px 20px rgba(0,0,0,0.1)', border: '1px solid rgba(0,0,0,0.1)', barText: '#334155', barDate: '#94a3b8', chartBorder: 'rgba(0,0,0,0.1)', }; } // ============ 工具函数 ============ function fmt(n) { if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B'; if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'; if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'; return n.toLocaleString(); } function pct(v) { return (v * 100).toFixed(1) + '%'; } function yuan(v) { return '¥' + v.toFixed(2); } function escapeHTML(value) { return String(value ?? '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[ch])); } function formatStatsDate(timestamp) { return new Date((timestamp + STATS_TZ_OFFSET_SECONDS) * 1000).toISOString().slice(0, 10); } function buildUsageUrl(capturedUrl, start, end) { const url = new URL(capturedUrl, window.location?.origin || 'https://platform.deepseek.com'); url.searchParams.set('start', String(start)); url.searchParams.set('end', String(end)); url.searchParams.set('tz', String(STATS_TZ_OFFSET_SECONDS)); return url.toString(); } // ============ 数据缓存 ============ let cachedAmount = null; let cachedCost = null; let capturedAmountUrl = null; let capturedCostUrl = null; let refreshInterval = 300000; let refreshTimer = null; let lastUrl = location.href; function isUsagePage() { return location.origin === 'https://platform.deepseek.com' && location.pathname === '/usage'; } function removeDashboard() { document.getElementById('ds-enhanced-stats')?.remove(); if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } } function handleRouteChange() { if (lastUrl === location.href) return; lastUrl = location.href; if (!isUsagePage()) removeDashboard(); else tryInject(); } // ============ 真实数据刷新 ============ async function fetchData() { if (!capturedAmountUrl && !capturedCostUrl) return; const now = Math.floor(Date.now() / 1000); const thirtyDaysAgo = now - 30 * 86400; try { if (capturedAmountUrl) { const r = await origFetch(buildUsageUrl(capturedAmountUrl, thirtyDaysAgo, now), { credentials: 'include' }); const j = await r.json(); if (j.code === 0) cachedAmount = j.data?.biz_data || j.data; } if (capturedCostUrl) { const r = await origFetch(buildUsageUrl(capturedCostUrl, thirtyDaysAgo, now), { credentials: 'include' }); const j = await r.json(); if (j.code === 0) cachedCost = j.data?.biz_data || j.data; } } catch (e) { console.warn('[DS Stats] fetchData error:', e); } } // ============ 拦截 fetch ============ const origFetch = window.fetch; window.fetch = function (...args) { const result = origFetch.apply(this, args); const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || ''; if (url.includes('/api/v0/usage/by_api_key/amount')) { if (!capturedAmountUrl) capturedAmountUrl = url; result.then(r => r.clone().json().then(j => { if (j.code === 0) { cachedAmount = j.data?.biz_data || j.data; tryInject(); } })).catch(() => {}); } if (url.includes('/api/v0/usage/by_api_key/cost')) { if (!capturedCostUrl) capturedCostUrl = url; result.then(r => r.clone().json().then(j => { if (j.code === 0) { cachedCost = j.data?.biz_data || j.data; tryInject(); } })).catch(() => {}); } return result; }; // ============ 拦截 XMLHttpRequest ============ const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._dsUrl = url; return origOpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function (...args) { this.addEventListener('load', function () { try { const data = JSON.parse(this.responseText); if (this._dsUrl?.includes('/api/v0/usage/by_api_key/amount') && data.code === 0) { if (!capturedAmountUrl) capturedAmountUrl = this._dsUrl; cachedAmount = data.data?.biz_data || data.data; tryInject(); } if (this._dsUrl?.includes('/api/v0/usage/by_api_key/cost') && data.code === 0) { if (!capturedCostUrl) capturedCostUrl = this._dsUrl; cachedCost = data.data?.biz_data || data.data; tryInject(); } } catch (e) { } }); return origSend.apply(this, args); }; // ============ 数据解析 ============ function parseData(amountData, costData) { if (!amountData || !costData) return null; const result = { keys: [], models: [], daily: {}, totalTokens: 0, totalRequests: 0, totalCost: 0 }; // 解析 amount:按日、按模型聚合 token 消耗 for (const series of amountData.series || []) { const keyId = series.api_key?.tracking_id || ''; const keyName = series.api_key?.name || 'unknown'; const modelName = series.model || 'unknown'; if (!result.keys.find(k => k.id === keyId)) result.keys.push({ id: keyId, name: keyName }); if (!result.models.includes(modelName)) result.models.push(modelName); for (const bucket of series.buckets || []) { const day = amountData.start <= bucket.time && bucket.time <= amountData.end ? formatStatsDate(bucket.time) : null; if (!day) continue; if (!result.daily[day]) result.daily[day] = { date: day, time: bucket.time, models: {}, totalTokens: 0, totalRequests: 0, cost: 0, promptCacheHit: 0, promptCacheMiss: 0 }; if (!result.daily[day].models[modelName]) result.daily[day].models[modelName] = { promptCacheHit: 0, promptCacheMiss: 0, responseToken: 0, requests: 0, keyCosts: {} }; const m = result.daily[day].models[modelName]; const u = bucket.usage || {}; m.promptCacheHit += u.PROMPT_CACHE_HIT_TOKEN || 0; m.promptCacheMiss += u.PROMPT_CACHE_MISS_TOKEN || 0; m.responseToken += u.RESPONSE_TOKEN || 0; m.requests += u.REQUEST || 0; result.daily[day].promptCacheHit += u.PROMPT_CACHE_HIT_TOKEN || 0; result.daily[day].promptCacheMiss += u.PROMPT_CACHE_MISS_TOKEN || 0; result.daily[day].totalTokens += (u.PROMPT_CACHE_HIT_TOKEN || 0) + (u.PROMPT_CACHE_MISS_TOKEN || 0) + (u.RESPONSE_TOKEN || 0); result.daily[day].totalRequests += u.REQUEST || 0; } } // 解析 cost:按日累加费用与 Key 维度 for (const currency of costData.data || []) { for (const series of currency.series || []) { const modelName = series.model || 'unknown'; for (const bucket of series.buckets || []) { const day = formatStatsDate(bucket.time); if (!result.daily[day]) continue; result.daily[day].cost += parseFloat(bucket.cost || '0'); if (result.daily[day].models[modelName]) { const keyId = series.api_key?.tracking_id || ''; result.daily[day].models[modelName].keyCosts[keyId] = (result.daily[day].models[modelName].keyCosts[keyId] || 0) + parseFloat(bucket.cost || '0'); } } } } // 汇总 const days = Object.values(result.daily); result.totalTokens = days.reduce((s, d) => s + d.totalTokens, 0); result.totalRequests = days.reduce((s, d) => s + d.totalRequests, 0); result.totalCost = days.reduce((s, d) => s + d.cost, 0); result.days = days.sort((a, b) => a.date.localeCompare(b.date)); // 每日指标 for (const day of result.days) { const dayInput = day.promptCacheHit + day.promptCacheMiss; day.hitRate = dayInput > 0 ? day.promptCacheHit / dayInput : 0; day.modelBreakdown = {}; for (const [modelName, mData] of Object.entries(day.models)) { const totalInput = mData.promptCacheHit + mData.promptCacheMiss; const hitRate = totalInput > 0 ? mData.promptCacheHit / totalInput : 0; day.modelBreakdown[modelName] = { hitRate, avgTokensPerReq: mData.requests > 0 ? (totalInput + mData.responseToken) / mData.requests : 0, ...mData }; } day.avgTokensPerReq = day.totalRequests > 0 ? day.totalTokens / day.totalRequests : 0; } // 模型维度汇总 const modelMap = {}; for (const day of result.days) { for (const [name, m] of Object.entries(day.models)) { if (!modelMap[name]) modelMap[name] = { hit: 0, miss: 0, output: 0, reqs: 0, cost: 0 }; modelMap[name].hit += m.promptCacheHit; modelMap[name].miss += m.promptCacheMiss; modelMap[name].output += m.responseToken; modelMap[name].reqs += m.requests; modelMap[name].cost += Object.values(m.keyCosts || {}).reduce((s, c) => s + c, 0); } } result.modelStats = Object.entries(modelMap) .map(([name, d]) => ({ name, ...d, total: d.hit + d.miss + d.output, hitRate: (d.hit + d.miss) > 0 ? d.hit / (d.hit + d.miss) : 0, })) .sort((a, b) => b.total - a.total); return result; } // ============ 渲染 ============ function metricBox(label, value, color, sub) { const t = theme(); return `
${label}
${value}
${sub ? `
${sub}
` : ''}
`; } function stackedBarChart(daily, label) { if (!daily || daily.length === 0) return ''; const t = theme(); const modelNames = [...new Set(daily.flatMap(d => Object.keys(d.models)))]; const maxVal = Math.max(...daily.map(d => d.totalTokens)); if (maxVal === 0) return ''; const MAX_BAR_H = 110; const bars = daily.map(d => { const colorMap = {}; modelNames.forEach((n, i) => { colorMap[n] = MODEL_COLORS[i % MODEL_COLORS.length]; }); let segs = ''; for (const name of modelNames) { const m = d.models[name]; const v = m ? m.promptCacheHit + m.promptCacheMiss + m.responseToken : 0; const segH = maxVal > 0 ? (v / maxVal) * MAX_BAR_H : 0; if (segH > 0) segs = `
` + segs; } const dateShort = d.date.slice(5); return `
${fmt(d.totalTokens)}
${segs}
${dateShort}
`; }).join(''); const legend = modelNames.map((n, i) => `${escapeHTML(MODEL_NAMES_CN[n] || n)}`).join(''); const minW = daily.length * 52; return `
${label}
${legend}
${bars}
`; } function simpleBarChart(daily, key, label, colorFn, displayFn) { if (!daily || daily.length === 0) return ''; const t = theme(); const maxVal = Math.max(...daily.map(d => d[key])); if (maxVal === 0) return ''; const MAX_BAR_H = 110; const bars = daily.map(d => { const val = d[key]; const h = maxVal > 0 ? (val / maxVal) * MAX_BAR_H : 0; const color = colorFn(d, val); const display = displayFn ? displayFn(val) : (key === 'hitRate' ? pct(val) : key === 'cost' ? '¥' + val.toFixed(2) : fmt(val)); const dateShort = d.date.slice(5); return `
${display}
${dateShort}
`; }).join(''); const minW = daily.length * 52; return `
${label}
${bars}
`; } function renderDashboard(data) { const t = theme(); const days = data.days; const today = days[days.length - 1] || { totalTokens: 0, totalRequests: 0, cost: 0, hitRate: 0, models: {} }; const totalHit = days.reduce((s, d) => s + Object.values(d.models).reduce((s2, m) => s2 + m.promptCacheHit, 0), 0); const totalMiss = days.reduce((s, d) => s + Object.values(d.models).reduce((s2, m) => s2 + m.promptCacheMiss, 0), 0); const overallHitRate = (totalHit + totalMiss) > 0 ? totalHit / (totalHit + totalMiss) : 0; const avgDailyCost = days.length > 0 ? data.totalCost / days.length : 0; const avgDailyReqs = days.length > 0 ? data.totalRequests / days.length : 0; const card = document.createElement('div'); card.id = 'ds-enhanced-stats'; card.style.cssText = `background:${t.bg};border-radius:12px;padding:20px 24px;margin:16px 0;color:${t.text};font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;box-shadow:${t.shadow};${t.border}`; // 模型明细卡片 const modelCards = data.modelStats.map((d, i) => { const color = MODEL_COLORS[i % MODEL_COLORS.length]; const totalPct = data.totalCost > 0 ? (d.cost / data.totalCost * 100).toFixed(1) + '%' : '0%'; return `
${escapeHTML(MODEL_NAMES_CN[d.name] || d.name)}
输入命中:${fmt(d.hit)}
输入未命中:${fmt(d.miss)}
输出:${fmt(d.output)}
缓存命中率:${pct(d.hitRate)}
Token 合计:${fmt(d.total)}
费用:${yuan(d.cost)}
请求次数:${fmt(d.reqs)}
占比:${totalPct}
`; }).join(''); // 刷新间隔选项 const intervals = [ { label: '1分钟', ms: 60000 }, { label: '5分钟', ms: 300000 }, { label: '10分钟', ms: 600000 }, { label: '30分钟', ms: 1800000 }, ]; const intervalBtns = intervals.map(iv => `${iv.label}` ).join(' '); // Key 对比数据 const keyCosts = {}; for (const day of days) { for (const mData of Object.values(day.models)) { for (const [keyId, cost] of Object.entries(mData.keyCosts || {})) { keyCosts[keyId] = (keyCosts[keyId] || 0) + cost; } } } card.innerHTML = `
📊 增强用量统计
${intervalBtns} 🔄 刷新 ${isDark ? '☀️ 亮色' : '🌙 暗色'}
${metricBox('今日费用', yuan(today.cost), '#fbbf24', fmt(today.totalRequests) + ' 次请求')} ${metricBox('今日 Token', fmt(today.totalTokens), '#c084fc', '均 ' + (today.totalRequests > 0 ? fmt(Math.round(today.totalTokens / today.totalRequests)) : '0') + ' /次')} ${metricBox('今日缓存命中率', today.hitRate > 0 ? pct(today.hitRate) : '-', today.hitRate > 0.9 ? '#4ade80' : '#facc15')} ${metricBox('30天总费用', yuan(data.totalCost), '#f59e0b', '日均 ' + yuan(avgDailyCost))} ${metricBox('30天总请求', fmt(data.totalRequests), '#f97316', '日均 ' + fmt(Math.round(avgDailyReqs)) + ' 次')}
${metricBox('30天总 Token', fmt(data.totalTokens), '#a78bfa', fmt(data.totalTokens / Math.max(days.length, 1)) + ' /天')} ${metricBox('30天整体命中率', pct(overallHitRate), overallHitRate > 0.9 ? '#4ade80' : '#facc15')} ${metricBox('模型数', data.modelStats.length, '#06b6d4', data.models.length + ' 个模型')} ${metricBox('API Key 数', data.keys.length, '#ec4899', (keyCosts ? Object.keys(keyCosts).length : 0) + ' 个使用')}
📦 模型消耗明细
${modelCards}
${stackedBarChart(days, '📈 每日 Token 消耗(按模型)')}
${simpleBarChart(days, 'cost', '💰 每日费用', (d, v) => d.date === today.date ? '#fbbf24' : '#d97706', v => '¥' + v.toFixed(2))}
${simpleBarChart(days, 'hitRate', '🎯 每日缓存命中率', (d, v) => v > 0.95 ? '#4ade80' : v > 0.9 ? '#86efac' : v > 0.8 ? '#facc15' : '#f87171', v => pct(v))}
${simpleBarChart(days, 'totalRequests', '📊 每日请求数', (d, v) => '#818cf8', v => fmt(v))}
🔑 API Key 使用量对比
${Object.entries(keyCosts).map(([keyId, cost]) => { const keyInfo = data.keys.find(k => k.id === keyId); const keyName = keyInfo?.name || keyId.slice(0, 8); const p = data.totalCost > 0 ? (cost / data.totalCost * 100).toFixed(1) : '0'; return `
${escapeHTML(keyName)}
消费${yuan(cost)}
占比${p}%
`; }).join('')}
`; return card; } // ============ 注入 ============ function findInsertTarget() { for (const el of document.querySelectorAll('*')) { if (el.children.length === 0 && el.textContent?.trim() === 'deepseek-v4-flash') { const container = el.closest('[class]'); if (container?.parentElement) return container.parentElement; } } const main = document.querySelector('main'); if (main?.lastElementChild) return main.lastElementChild; return document.body; } function inject(data) { if (!isUsagePage()) { removeDashboard(); return false; } if (document.getElementById('ds-enhanced-stats')) return true; const target = findInsertTarget(); const card = renderDashboard(data); if (target.nextSibling) { target.parentElement.insertBefore(card, target.nextSibling); } else { target.appendChild(card); } return true; } function tryInject() { if (!isUsagePage()) { removeDashboard(); return; } if (!cachedAmount || !cachedCost) return; const data = parseData(cachedAmount, cachedCost); if (!data) return; window.__dsMetrics = data; inject(data); setupControls(); startAutoRefresh(); const timeEl = document.getElementById('ds-refresh-time'); if (timeEl) timeEl.textContent = '更新于 ' + new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } // ============ 刷新控制 ============ function setupControls() { const intervalEl = document.getElementById('ds-refresh-interval'); const refreshBtn = document.getElementById('ds-refresh-btn'); const themeBtn = document.getElementById('ds-theme-btn'); if (intervalEl) { intervalEl.querySelectorAll('[data-ms]').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); refreshInterval = parseInt(btn.dataset.ms); startAutoRefresh(); btn.style.transform = 'scale(1.1)'; btn.style.transition = 'transform 0.15s'; setTimeout(() => { btn.style.transform = ''; }, 150); doRefresh(); }; }); } if (refreshBtn) { refreshBtn.onclick = async (e) => { e.stopPropagation(); refreshBtn.textContent = '⏳ 刷新中...'; refreshBtn.style.background = 'rgba(99,102,241,0.4)'; await doRefresh(); const btn = document.getElementById('ds-refresh-btn'); const timeEl = document.getElementById('ds-refresh-time'); if (btn) { btn.textContent = '✅ 已刷新'; btn.style.background = 'rgba(34,197,94,0.2)'; if (timeEl) timeEl.textContent = '更新于 ' + new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); setTimeout(() => { btn.textContent = '🔄 刷新'; btn.style.background = ''; }, 1500); } }; } if (themeBtn) { themeBtn.onclick = (e) => { e.stopPropagation(); isDark = !isDark; doRefresh(); }; } } function startAutoRefresh() { if (refreshTimer) clearInterval(refreshTimer); refreshTimer = setInterval(() => doRefresh(), refreshInterval); } async function doRefresh() { if (!isUsagePage()) { removeDashboard(); return; } await fetchData(); if (cachedAmount && cachedCost) { const data = parseData(cachedAmount, cachedCost); if (data) { window.__dsMetrics = data; const existing = document.getElementById('ds-enhanced-stats'); if (existing) { const card = renderDashboard(data); existing.innerHTML = card.innerHTML; existing.style.cssText = card.style.cssText; } else { inject(data); } setupControls(); const timeEl = document.getElementById('ds-refresh-time'); if (timeEl) timeEl.textContent = '更新于 ' + new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } } } // ============ 监听主题变化 ============ const observer = new MutationObserver(() => { handleRouteChange(); if (!isUsagePage()) return; if (window.__dsMetrics && !document.getElementById('ds-enhanced-stats')) { inject(window.__dsMetrics); setupControls(); } }); if (document.body) observer.observe(document.body, { childList: true, subtree: true }); else document.addEventListener('DOMContentLoaded', () => observer.observe(document.body, { childList: true, subtree: true })); window.addEventListener('popstate', handleRouteChange); setInterval(handleRouteChange, 500); console.log('[DS Stats] v1.0 启动'); })();