【油猴脚本】ds 面板缓存命中自动计算器

最近也是 DeepSeek 用起来了,看到数据的时候总忍不住想算算缓存命中的情况,但是手动算是不可能滴,脚本启动!

ds 缓存计算脚本
// ==UserScript==
// @name         DeepSeek Usage - Cache Hit Rate Monitor
// @namespace    https://platform.deepseek.com
// @version      1.0.0
// @description  监测 DeepSeek 用量页面的 Prompt 缓存命中率,以双胶囊形式显示累积命中率和单日命中率(hover 图表时自动更新单日数据)
// @author       DeepSeek-V4-Pro
// @match        https://platform.deepseek.com/*
// @grant        none
// @connect      platform.deepseek.com
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ========== State ==========
    var modelData = new Map();          // Map<modelName, {total, daily}>
    var uiUpdateTimer = null;
    var dailyDate = '';                 // currently shown daily date (yyyy-mm-dd)

    // ========== Utility ==========
    function computeRate(hit, miss) {
        var total = hit + miss;
        if (total === 0) return null;
        return (hit / total) * 100;
    }

    function getAmount(usageArr, type) {
        var entry = (usageArr || []).find(function(u) { return u.type === type; });
        return entry ? parseInt(entry.amount, 10) || 0 : 0;
    }

    function formatRate(rate) {
        if (rate === null || rate === undefined) return 'All Cache: N/A';
        return 'All Cache: ' + rate.toFixed(1) + '%';
    }

    function rateClass(rate) {
        if (rate === null || rate === undefined) return 'cache-na';
        if (rate >= 80) return 'cache-high';
        if (rate >= 50) return 'cache-mid';
        return '';
    }

    function todayStr() {
        var d = new Date();
        var m = d.getMonth() + 1;
        var day = d.getDate();
        return d.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day;
    }

    function dateLabel(iso) {
        // 2026-05-15 -> 5/15
        var parts = iso.split('-');
        return parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10);
    }

    function getDefaultDailyDate() {
        // Prefer today, fallback to last day in data
        var t = todayStr();
        var iter = modelData.values();
        var entry = iter.next();
        while (entry && !entry.done) {
            if (entry.value.daily.has(t)) return t;
            entry = iter.next();
        }
        // No today, pick the max date from first model
        var first = modelData.values().next();
        if (first.done) return t;
        var dates = Array.from(first.value.daily.keys()).sort();
        return dates.length > 0 ? dates[dates.length - 1] : t;
    }

    // ========== Data Processing (called by interceptors) ==========
    function processApiData(bizData) {
        var dataMap = new Map();

        // Cumulative (total) data
        (bizData.total || []).forEach(function(entry) {
            var name = entry.model;
            var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
            var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
            dataMap.set(name, {
                total: { hit: hit, miss: miss, rate: computeRate(hit, miss) },
                daily: new Map()
            });
        });

        // Daily data
        (bizData.days || []).forEach(function(day) {
            var date = day.date;
            (day.data || []).forEach(function(entry) {
                var name = entry.model;
                if (!dataMap.has(name)) return;
                var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
                var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
                dataMap.get(name).daily.set(date, {
                    hit: hit, miss: miss, rate: computeRate(hit, miss)
                });
            });
        });

        return dataMap;
    }

    function scheduleUIUpdate() {
        clearTimeout(uiUpdateTimer);
        uiUpdateTimer = setTimeout(updateUI, 300);
    }

    function updateUI() {
        if (modelData.size === 0) return;
        clearTimeout(resetTimer);
        resetTimer = null;
        dailyDate = getDefaultDailyDate();
        injectAllCapsules();
    }

    // ========== XHR + fetch Interception ==========
    function handleApiResponse(json) {
        if (!json || json.code !== 0 || !json.data || !json.data.biz_data) return;
        var bizData = json.data.biz_data;
        modelData = processApiData(bizData);
        scheduleUIUpdate();
    }

    // --- Intercept XMLHttpRequest ---
    var origSend = XMLHttpRequest.prototype.send;
    var origOpen = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._ds_url = url;
        return origOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function() {
        var self = this;
        var url = self._ds_url;
        if (url && url.indexOf('/usage/amount') !== -1) {
            self.addEventListener('load', function() {
                try {
                    var json = JSON.parse(self.responseText);
                    handleApiResponse(json);
                } catch(e) {}
            });
        }
        return origSend.apply(this, arguments);
    };

    // --- Intercept fetch ---
    if (typeof window.fetch !== 'undefined') {
        var origFetch = window.fetch;
        window.fetch = function(url, options) {
            return origFetch.apply(this, arguments).then(function(response) {
                if (typeof url === 'string' && url.indexOf('/usage/amount') !== -1) {
                    var cloned = response.clone();
                    cloned.json().then(function(json) {
                        handleApiResponse(json);
                    }).catch(function() {});
                }
                return response;
            });
        };
    }

    // ========== DOM-dependent code (deferred) ==========
    function onDomReady() {
        injectCSS();
        setupGlobalTooltipObserver();
        waitForReact();
    }

    function injectCSS() {
        var s = document.createElement('style');
        s.textContent =
            '.ds-cache-hit-badge{' +
            'display:inline-block;padding:2px 10px;margin-left:8px;' +
            'font-size:12px;line-height:1.5;font-weight:500;' +
            'border-radius:20px;white-space:nowrap;vertical-align:middle;' +
            'transition:background 0.15s ease;' +
            'color:rgb(var(--ds-rgb-primary,0 0 0));' +
            'background:rgb(var(--ds-rgb-elevated,255 255 255));' +
            'border:1px solid rgb(var(--ds-rgb-separator,210 216 229));}' +
            '.ds-cache-hit-badge.cache-na{' +
            'color:rgb(var(--ds-rgb-label-2,144 148 162));border-style:dashed;}' +
            '.ds-cache-hit-badge.cache-high{color:#16A34A;border-color:#16A34A;}' +
            '.ds-cache-hit-badge.cache-mid{color:#D97706;border-color:#D97706;}';
        (document.head || document.documentElement).appendChild(s);
    }

    function waitForReact() {
        var attempts = 0;
        function check() {
            var sel = document.querySelector('select.ds-native-select__select');
            var spans = document.querySelectorAll('span.ds-text--monospace.ds-text--fm.ds-text--linherit');
            if (sel && spans.length > 0) {
                setupMonthWatcher();
                setupPageObserver();
                // If we already have data (interceptor caught the API call), update UI now
                if (modelData.size > 0) {
                    updateUI();
                } else {
                    injectAllCapsules();
                }
                return;
            }
            if (++attempts < 20) {
                setTimeout(check, 500);
            }
        }
        check();
    }

    // ========== DOM Query ==========
    function findTokenSections() {
        var modelSpans = document.querySelectorAll(
            'span.ds-text--monospace.ds-text--fm.ds-text--linherit');
        var sections = [];

        modelSpans.forEach(function(span) {
            var modelName = span.textContent.trim();
            var headerBar = span.closest('._6926780');
            if (!headerBar) return;

            var chartBody = headerBar.nextElementSibling;
            if (!chartBody || !chartBody.classList.contains('_06da35f')) return;

            var outerFlexes = chartBody.querySelectorAll(
                '.ds-flex[style*="flex-direction: column"]');
            var tokenContainer = null;
            var echartsDiv = null;

            outerFlexes.forEach(function(outerFlex) {
                var innerFlexes = outerFlex.querySelectorAll(
                    '.ds-flex[style*="align-items: baseline"]');
                innerFlexes.forEach(function(innerFlex) {
                    var label = innerFlex.querySelector('span.ds-text--fsp.ds-text--lsp');
                    if (label && label.textContent.trim() === 'Tokens') {
                        tokenContainer = innerFlex;
                        echartsDiv = outerFlex.querySelector('[_echarts_instance_]');
                    }
                });
                if (!echartsDiv) {
                    echartsDiv = outerFlex.querySelector('[_echarts_instance_]');
                }
            });

            if (tokenContainer) {
                sections.push({
                    modelName: modelName,
                    tokenContainer: tokenContainer,
                    echartsDiv: echartsDiv
                });
            }
        });

        return sections;
    }

    // ========== Capsule Badge Injection ==========
    function createBadge(text, cssClass) {
        var badge = document.createElement('span');
        badge.className = 'ds-cache-hit-badge ' + cssClass;
        badge.textContent = text;
        return badge;
    }

    function injectCapsule(section, data, dailyDateOverride) {
        var container = section.tokenContainer;
        // Remove old badges
        var old = container.querySelectorAll('.ds-cache-hit-badge');
        old.forEach(function(el) { el.remove(); });

        // Cumulative capsule
        var rate = data ? data.total.rate : null;
        container.appendChild(createBadge(formatRate(rate), rateClass(rate)));

        // Daily capsule
        var date = dailyDateOverride || dailyDate;
        var dailyRate = null;
        if (data && date) {
            var d = data.daily.get(date);
            if (d) dailyRate = d.rate;
        }
        var dailyText = (dailyRate !== null && dailyRate !== undefined)
            ? dateLabel(date) + ': ' + dailyRate.toFixed(1) + '%'
            : dateLabel(date) + ': N/A';
        container.appendChild(createBadge(dailyText, rateClass(dailyRate)));
    }

    function injectAllCapsules() {
        var sections = findTokenSections();
        sections.forEach(function(section) {
            var data = modelData.get(section.modelName);
            injectCapsule(section, data);
        });
    }

    // ========== Tooltip Date Reading ==========
    function getDateFromTooltip(tooltip) {
        var flex = tooltip.querySelector('div[style*="display: flex"]');
        if (!flex) return null;
        var leftCol = flex.children[0];
        if (!leftCol || !leftCol.children[0]) return null;
        return leftCol.children[0].textContent.trim();
    }

    // ========== Global Tooltip Observer (reads date → updates daily capsule) ==========
    var tipCheckTimer = null;
    var resetTimer = null;

    function checkAllTooltips() {
        var sections = findTokenSections();
        var foundDate = null;
        sections.forEach(function(section) {
            if (foundDate || !section.echartsDiv) return;
            var tooltips = section.echartsDiv.querySelectorAll('div[style*="absolute"]');
            tooltips.forEach(function(tooltip) {
                if (foundDate) return;
                var cs = window.getComputedStyle(tooltip);
                if (cs.visibility === 'hidden' || cs.opacity === '0') return;
                var d = getDateFromTooltip(tooltip);
                if (d) foundDate = d;
            });
        });

        if (foundDate) {
            clearTimeout(resetTimer);
            resetTimer = null;
            if (foundDate !== dailyDate) {
                dailyDate = foundDate;
                injectAllCapsules();
            }
        } else if (dailyDate && dailyDate !== getDefaultDailyDate()) {
            // Mouse left chart — schedule reset to today after idle period
            if (!resetTimer) {
                resetTimer = setTimeout(function() {
                    resetTimer = null;
                    dailyDate = getDefaultDailyDate();
                    injectAllCapsules();
                }, 1500);
            }
        }
    }

    function setupGlobalTooltipObserver() {
        var root = document.getElementById('root') || document.body;
        if (!root) return;
        var observer = new MutationObserver(function() {
            if (tipCheckTimer) return;
            tipCheckTimer = setTimeout(function() {
                tipCheckTimer = null;
                checkAllTooltips();
            }, 60);
        });
        observer.observe(root, {
            subtree: true,
            attributes: true,
            attributeFilter: ['style']
        });
    }

    // ========== Month Select Watcher ==========
    function setupMonthWatcher() {
        var sel = document.querySelector('select.ds-native-select__select');
        if (!sel) return;
        sel.addEventListener('change', function() {
            setTimeout(function() {
                if (modelData.size > 0) {
                    setTimeout(function() {
                        injectAllCapsules();
                    }, 500);
                }
            }, 300);
        });
    }

    // ========== Page Observer (SPA re-render detection) ==========
    function setupPageObserver() {
        var main = document.querySelector('main') || document.getElementById('root');
        if (!main) return;

        var debounceTimer = null;
        new MutationObserver(function() {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(function() {
                injectAllCapsules();
                checkAllTooltips();
            }, 400);
        }).observe(main, { childList: true, subtree: true });
    }

    // ========== Entry ==========
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', onDomReady);
    } else {
        onDomReady();
    }
})();

效果如图:

带消费记录版本
// ==UserScript==
// @name         DeepSeek Usage - Cache Hit Rate Monitor
// @namespace    https://platform.deepseek.com
// @version      1.3.0
// @description  监测 DeepSeek 用量页面的 Prompt 缓存命中率,以双胶囊形式显示累积命中率和单日命中率,并在日胶囊中同时显示当天消耗金额(hover 图表时自动更新单日数据)
// @match        https://platform.deepseek.com/usage
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ========== State ==========
    var modelData = new Map();          // Map<modelName, {total, daily}>
    var costData = new Map();           // Map<modelName, {total:Number, daily:Map<date,Number>}>
    var costCurrency = '';              // 'CNY' | 'USD'
    var uiUpdateTimer = null;
    var dailyDate = '';                 // currently shown daily date (yyyy-mm-dd)

    // ========== Utility ==========
    function computeRate(hit, miss) {
        var total = hit + miss;
        if (total === 0) return null;
        return (hit / total) * 100;
    }

    function getAmount(usageArr, type) {
        var entry = (usageArr || []).find(function(u) { return u.type === type; });
        return entry ? parseInt(entry.amount, 10) || 0 : 0;
    }

    function formatRate(rate) {
        if (rate === null || rate === undefined) return 'All Cache: N/A';
        return 'All Cache: ' + rate.toFixed(1) + '%';
    }

    function rateClass(rate) {
        if (rate === null || rate === undefined) return 'cache-na';
        if (rate >= 80) return 'cache-high';
        if (rate >= 50) return 'cache-mid';
        return '';
    }

    function todayStr() {
        var d = new Date();
        var m = d.getMonth() + 1;
        var day = d.getDate();
        return d.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day;
    }

    function dateLabel(iso) {
        // 2026-05-15 -> 5/15
        var parts = iso.split('-');
        return parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10);
    }

    function getDefaultDailyDate() {
        // Prefer today, fallback to last day in data
        var t = todayStr();
        var iter = modelData.values();
        var entry = iter.next();
        while (entry && !entry.done) {
            if (entry.value.daily.has(t)) return t;
            entry = iter.next();
        }
        // No today, pick the max date from first model
        var first = modelData.values().next();
        if (first.done) return t;
        var dates = Array.from(first.value.daily.keys()).sort();
        return dates.length > 0 ? dates[dates.length - 1] : t;
    }

    // ========== Cost Helpers ==========
    function sumUsage(usageArr) {
        return (usageArr || []).reduce(function(s, u) {
            var v = parseFloat(u.amount);
            return s + (isNaN(v) ? 0 : v);
        }, 0);
    }

    function currencySymbol(c) {
        return c === 'USD' ? '$' : '¥';
    }

    function formatCost(n) {
        if (n === null || n === undefined) return null;
        if (n === 0) return '0';
        if (n < 0.01) return n.toFixed(4);
        return n.toFixed(2);
    }

    function pickCurrencyEntry(bizDataArr) {
        if (!Array.isArray(bizDataArr) || bizDataArr.length === 0) return null;
        var sums = bizDataArr.map(function(entry) {
            return (entry.total || []).reduce(function(s, m) {
                return s + sumUsage(m.usage);
            }, 0);
        });
        // Prefer non-zero entry; if multiple non-zero, prefer CNY
        var pick = null;
        for (var i = 0; i < bizDataArr.length; i++) {
            if (sums[i] > 0) {
                if (!pick) pick = bizDataArr[i];
                else if (bizDataArr[i].currency === 'CNY') pick = bizDataArr[i];
            }
        }
        if (pick) return pick;
        // All zero — prefer CNY entry, else first
        for (var j = 0; j < bizDataArr.length; j++) {
            if (bizDataArr[j].currency === 'CNY') return bizDataArr[j];
        }
        return bizDataArr[0];
    }

    // ========== Data Processing (called by interceptors) ==========
    function processApiData(bizData) {
        var dataMap = new Map();

        // Cumulative (total) data
        (bizData.total || []).forEach(function(entry) {
            var name = entry.model;
            var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
            var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
            dataMap.set(name, {
                total: { hit: hit, miss: miss, rate: computeRate(hit, miss) },
                daily: new Map()
            });
        });

        // Daily data
        (bizData.days || []).forEach(function(day) {
            var date = day.date;
            (day.data || []).forEach(function(entry) {
                var name = entry.model;
                if (!dataMap.has(name)) return;
                var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
                var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
                dataMap.get(name).daily.set(date, {
                    hit: hit, miss: miss, rate: computeRate(hit, miss)
                });
            });
        });

        return dataMap;
    }

    function processCostData(bizDataArr) {
        var entry = pickCurrencyEntry(bizDataArr);
        if (!entry) return null;
        var map = new Map();
        (entry.total || []).forEach(function(m) {
            map.set(m.model, { total: sumUsage(m.usage), daily: new Map() });
        });
        (entry.days || []).forEach(function(day) {
            (day.data || []).forEach(function(m) {
                if (!map.has(m.model)) return;
                map.get(m.model).daily.set(day.date, sumUsage(m.usage));
            });
        });
        return { data: map, currency: entry.currency || 'CNY' };
    }

    function scheduleUIUpdate() {
        clearTimeout(uiUpdateTimer);
        uiUpdateTimer = setTimeout(updateUI, 300);
    }

    function updateUI() {
        if (modelData.size === 0) return;
        clearTimeout(resetTimer);
        resetTimer = null;
        dailyDate = getDefaultDailyDate();
        injectAllCapsules();
    }

    // ========== XHR + fetch Interception ==========
    function handleAmountApiResponse(json) {
        if (!json || json.code !== 0 || !json.data || !json.data.biz_data) return;
        var bizData = json.data.biz_data;
        modelData = processApiData(bizData);
        scheduleUIUpdate();
    }

    function handleCostApiResponse(json) {
        if (!json || json.code !== 0 || !json.data || !json.data.biz_data) return;
        var result = processCostData(json.data.biz_data);
        if (!result) return;
        costData = result.data;
        costCurrency = result.currency;
        scheduleUIUpdate();
    }

    function routeApiResponse(url, json) {
        if (!url) return;
        if (url.indexOf('/usage/amount') !== -1) handleAmountApiResponse(json);
        else if (url.indexOf('/usage/cost') !== -1) handleCostApiResponse(json);
    }

    // --- Intercept XMLHttpRequest ---
    var origSend = XMLHttpRequest.prototype.send;
    var origOpen = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._ds_url = url;
        return origOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function() {
        var self = this;
        var url = self._ds_url;
        if (url && (url.indexOf('/usage/amount') !== -1 || url.indexOf('/usage/cost') !== -1)) {
            self.addEventListener('load', function() {
                try {
                    var json = JSON.parse(self.responseText);
                    routeApiResponse(url, json);
                } catch(e) {}
            });
        }
        return origSend.apply(this, arguments);
    };

    // --- Intercept fetch ---
    if (typeof window.fetch !== 'undefined') {
        var origFetch = window.fetch;
        window.fetch = function(url, options) {
            return origFetch.apply(this, arguments).then(function(response) {
                if (typeof url === 'string' &&
                    (url.indexOf('/usage/amount') !== -1 || url.indexOf('/usage/cost') !== -1)) {
                    var cloned = response.clone();
                    cloned.json().then(function(json) {
                        routeApiResponse(url, json);
                    }).catch(function() {});
                }
                return response;
            });
        };
    }

    // ========== DOM-dependent code (deferred) ==========
    function onDomReady() {
        injectCSS();
        setupGlobalTooltipObserver();
        waitForReact();
    }

    function injectCSS() {
        var s = document.createElement('style');
        s.textContent =
            '.ds-cache-hit-badge{' +
            'display:inline-block;padding:2px 10px;margin-left:8px;' +
            'font-size:12px;line-height:1.5;font-weight:500;' +
            'border-radius:20px;white-space:nowrap;vertical-align:middle;' +
            'transition:background 0.15s ease;' +
            'color:rgb(var(--ds-rgb-primary,0 0 0));' +
            'background:rgb(var(--ds-rgb-elevated,255 255 255));' +
            'border:1px solid rgb(var(--ds-rgb-separator,210 216 229));}' +
            '.ds-cache-hit-badge.cache-na{' +
            'color:rgb(var(--ds-rgb-label-2,144 148 162));border-style:dashed;}' +
            '.ds-cache-hit-badge.cache-high{color:#16A34A;border-color:#16A34A;}' +
            '.ds-cache-hit-badge.cache-mid{color:#D97706;border-color:#D97706;}';
        (document.head || document.documentElement).appendChild(s);
    }

    function waitForReact() {
        var attempts = 0;
        function check() {
            var sel = document.querySelector('select.ds-native-select__select');
            var spans = document.querySelectorAll('span.ds-text--monospace.ds-text--fm.ds-text--linherit');
            if (sel && spans.length > 0) {
                setupMonthWatcher();
                setupPageObserver();
                // If we already have data (interceptor caught the API call), update UI now
                if (modelData.size > 0) {
                    updateUI();
                } else {
                    injectAllCapsules();
                }
                return;
            }
            if (++attempts < 20) {
                setTimeout(check, 500);
            }
        }
        check();
    }

    // ========== DOM Query ==========
    function findTokenSections() {
        var modelSpans = document.querySelectorAll(
            'span.ds-text--monospace.ds-text--fm.ds-text--linherit');
        var sections = [];

        modelSpans.forEach(function(span) {
            var modelName = span.textContent.trim();
            var headerBar = span.closest('._6926780');
            if (!headerBar) return;

            var chartBody = headerBar.nextElementSibling;
            if (!chartBody || !chartBody.classList.contains('_06da35f')) return;

            var outerFlexes = chartBody.querySelectorAll(
                '.ds-flex[style*="flex-direction: column"]');
            var tokenContainer = null;
            var echartsDiv = null;

            outerFlexes.forEach(function(outerFlex) {
                var innerFlexes = outerFlex.querySelectorAll(
                    '.ds-flex[style*="align-items: baseline"]');
                innerFlexes.forEach(function(innerFlex) {
                    var label = innerFlex.querySelector('span.ds-text--fsp.ds-text--lsp');
                    if (label && label.textContent.trim() === 'Tokens') {
                        tokenContainer = innerFlex;
                        echartsDiv = outerFlex.querySelector('[_echarts_instance_]');
                    }
                });
                if (!echartsDiv) {
                    echartsDiv = outerFlex.querySelector('[_echarts_instance_]');
                }
            });

            if (tokenContainer) {
                sections.push({
                    modelName: modelName,
                    tokenContainer: tokenContainer,
                    echartsDiv: echartsDiv
                });
            }
        });

        return sections;
    }

    // ========== Capsule Badge Injection ==========
    function createBadge(text, cssClass) {
        var badge = document.createElement('span');
        badge.className = 'ds-cache-hit-badge ' + cssClass;
        badge.textContent = text;
        return badge;
    }

    function injectCapsule(section, data, dailyDateOverride) {
        var container = section.tokenContainer;
        // Remove old badges
        var old = container.querySelectorAll('.ds-cache-hit-badge');
        old.forEach(function(el) { el.remove(); });

        // Cumulative capsule
        var rate = data ? data.total.rate : null;
        container.appendChild(createBadge(formatRate(rate), rateClass(rate)));

        // Daily capsule
        var date = dailyDateOverride || dailyDate;
        var dailyRate = null;
        if (data && date) {
            var d = data.daily.get(date);
            if (d) dailyRate = d.rate;
        }
        var dailyText = (dailyRate !== null && dailyRate !== undefined)
            ? dateLabel(date) + ': ' + dailyRate.toFixed(1) + '%'
            : dateLabel(date) + ': N/A';

        // Append daily cost
        var costEntry = costData.get(section.modelName);
        if (costEntry && date) {
            var dailyCost = costEntry.daily.get(date);
            var costStr = formatCost(dailyCost);
            if (costStr !== null) {
                dailyText += ' / ' + currencySymbol(costCurrency) + costStr;
            }
        }
        container.appendChild(createBadge(dailyText, rateClass(dailyRate)));
    }

    function injectAllCapsules() {
        var sections = findTokenSections();
        sections.forEach(function(section) {
            var data = modelData.get(section.modelName);
            injectCapsule(section, data);
        });
    }

    // ========== Tooltip Date Reading ==========
    function getDateFromTooltip(tooltip) {
        var flex = tooltip.querySelector('div[style*="display: flex"]');
        if (!flex) return null;
        var leftCol = flex.children[0];
        if (!leftCol || !leftCol.children[0]) return null;
        return leftCol.children[0].textContent.trim();
    }

    // ========== Global Tooltip Observer (reads date → updates daily capsule) ==========
    var tipCheckTimer = null;
    var resetTimer = null;

    function checkAllTooltips() {
        var sections = findTokenSections();
        var foundDate = null;
        sections.forEach(function(section) {
            if (foundDate || !section.echartsDiv) return;
            var tooltips = section.echartsDiv.querySelectorAll('div[style*="absolute"]');
            tooltips.forEach(function(tooltip) {
                if (foundDate) return;
                var cs = window.getComputedStyle(tooltip);
                if (cs.visibility === 'hidden' || cs.opacity === '0') return;
                var d = getDateFromTooltip(tooltip);
                if (d) foundDate = d;
            });
        });

        if (foundDate) {
            clearTimeout(resetTimer);
            resetTimer = null;
            if (foundDate !== dailyDate) {
                dailyDate = foundDate;
                injectAllCapsules();
            }
        } else if (dailyDate && dailyDate !== getDefaultDailyDate()) {
            // Mouse left chart — schedule reset to today after idle period
            if (!resetTimer) {
                resetTimer = setTimeout(function() {
                    resetTimer = null;
                    dailyDate = getDefaultDailyDate();
                    injectAllCapsules();
                }, 1500);
            }
        }
    }

    function setupGlobalTooltipObserver() {
        var root = document.getElementById('root') || document.body;
        if (!root) return;
        var observer = new MutationObserver(function() {
            if (tipCheckTimer) return;
            tipCheckTimer = setTimeout(function() {
                tipCheckTimer = null;
                checkAllTooltips();
            }, 60);
        });
        observer.observe(root, {
            subtree: true,
            attributes: true,
            attributeFilter: ['style']
        });
    }

    // ========== Month Select Watcher ==========
    function setupMonthWatcher() {
        var sel = document.querySelector('select.ds-native-select__select');
        if (!sel) return;
        sel.addEventListener('change', function() {
            setTimeout(function() {
                if (modelData.size > 0) {
                    setTimeout(function() {
                        injectAllCapsules();
                    }, 500);
                }
            }, 300);
        });
    }

    // ========== Page Observer (SPA re-render detection) ==========
    function setupPageObserver() {
        var main = document.querySelector('main') || document.getElementById('root');
        if (!main) return;

        var debounceTimer = null;
        new MutationObserver(function() {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(function() {
                injectAllCapsules();
                checkAllTooltips();
            }, 400);
        }).observe(main, { childList: true, subtree: true });
    }

    // ========== Entry ==========
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', onDomReady);
    } else {
        onDomReady();
    }
})();

8 个赞

感谢佬,用上了已经

2 个赞

deepseek platform 官网进行了页面更新,原来的脚本失效了,根据新页面更新了脚本如下:

// ==UserScript==
// @name         DeepSeek Usage - Cache Hit Rate Monitor
// @namespace    https://platform.deepseek.com
// @version      1.5.0
// @description  监测 DeepSeek 用量页面的 Prompt 缓存命中率,以双胶囊形式显示累积命中率和单日命中率,并在日胶囊中同时显示当天消耗金额(hover 图表时自动更新单日数据)
// @match        https://platform.deepseek.com/usage*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ========== State ==========
    var modelData = new Map();          // Map<modelName, {total, daily}>
    var costData = new Map();           // Map<modelName, {total:Number, daily:Map<date,Number>}>
    var costCurrency = '';              // 'CNY' | 'USD'
    var uiUpdateTimer = null;
    var dailyDate = '';                 // currently shown daily date (yyyy-mm-dd)

    // ========== Utility ==========
    function computeRate(hit, miss) {
        var total = hit + miss;
        if (total === 0) return null;
        return (hit / total) * 100;
    }

    function toNumber(value) {
        var n = parseFloat(value);
        return isNaN(n) ? 0 : n;
    }

    function getAmount(usage, type) {
        if (Array.isArray(usage)) {
            var entry = usage.find(function(u) { return u.type === type; });
            return entry ? toNumber(entry.amount) : 0;
        }
        if (usage && Object.prototype.hasOwnProperty.call(usage, type)) {
            return toNumber(usage[type]);
        }
        return 0;
    }

    function bucketTimeToDate(time) {
        var d = new Date(toNumber(time) * 1000);
        var y = d.getUTCFullYear();
        var m = d.getUTCMonth() + 1;
        var day = d.getUTCDate();
        return y + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day;
    }

    function ensureModelEntry(map, modelName) {
        if (!map.has(modelName)) {
            map.set(modelName, {
                total: { hit: 0, miss: 0, rate: null },
                daily: new Map()
            });
        }
        return map.get(modelName);
    }
    function formatRate(rate) {
        if (rate === null || rate === undefined) return 'All Cache: N/A';
        return 'All Cache: ' + rate.toFixed(1) + '%';
    }

    function rateClass(rate) {
        if (rate === null || rate === undefined) return 'cache-na';
        if (rate >= 80) return 'cache-high';
        if (rate >= 50) return 'cache-mid';
        return '';
    }

    function todayStr() {
        var d = new Date();
        var m = d.getMonth() + 1;
        var day = d.getDate();
        return d.getFullYear() + '-' + (m < 10 ? '0' : '') + m + '-' + (day < 10 ? '0' : '') + day;
    }

    function dateLabel(iso) {
        // 2026-05-15 -> 5/15
        var parts = iso.split('-');
        return parseInt(parts[1], 10) + '/' + parseInt(parts[2], 10);
    }

    function getDefaultDailyDate() {
        // Prefer today only when it has cache-token data; otherwise use the latest active day.
        var t = todayStr();
        var latestActive = '';
        var iter = modelData.values();
        var entry = iter.next();

        function hasCacheTokens(day) {
            return day && (day.hit + day.miss) > 0;
        }

        while (entry && !entry.done) {
            if (hasCacheTokens(entry.value.daily.get(t))) return t;
            entry.value.daily.forEach(function(day, date) {
                if (hasCacheTokens(day) && (!latestActive || date > latestActive)) latestActive = date;
            });
            entry = iter.next();
        }

        return latestActive || t;
    }
    function normalizeText(text) {
        return (text || '').replace(/\s+/g, ' ').trim();
    }

    function findAnyModelName(text) {
        var normalized = normalizeText(text);
        var keys = Array.from(modelData.keys());

        for (var i = 0; i < keys.length; i++) {
            if (normalized === keys[i] || normalized.indexOf(keys[i]) !== -1) return keys[i];
        }

        if (/^deepseek[-a-z0-9_]+$/i.test(normalized)) return normalized;
        return null;
    }

    function resolveModelName(section) {
        if (!section) return null;
        if (section.modelName) return section.modelName;

        var current = section.tokenContainer || section;
        while (current && current !== document.body) {
            var prev = current.previousElementSibling;
            if (prev) {
                var candidate = findAnyModelName(prev.textContent);
                if (candidate) {
                    section.modelName = candidate;
                    return candidate;
                }
                if (prev.querySelectorAll) {
                    var leafs = prev.querySelectorAll('span, div');
                    for (var i = 0; i < leafs.length; i++) {
                        candidate = findAnyModelName(leafs[i].textContent);
                        if (candidate) {
                            section.modelName = candidate;
                            return candidate;
                        }
                    }
                }
            }
            current = current.parentElement;
        }

        return section.modelName || null;
    }
    // ========== Cost Helpers ==========
    function sumUsage(usage) {
        if (Array.isArray(usage)) {
            return usage.reduce(function(s, u) {
                return s + toNumber(u.amount);
            }, 0);
        }
        if (usage && typeof usage === 'object') {
            return Object.keys(usage).reduce(function(s, key) {
                return s + toNumber(usage[key]);
            }, 0);
        }
        return toNumber(usage);
    }
    function currencySymbol(c) {
        return c === 'USD' ? '$' : '¥';
    }

    function formatCost(n) {
        if (n === null || n === undefined) return null;
        if (n === 0) return '0';
        if (n < 0.01) return n.toFixed(4);
        return n.toFixed(2);
    }

    function pickCurrencyEntry(bizDataArr) {
        if (!Array.isArray(bizDataArr) || bizDataArr.length === 0) return null;
        var sums = bizDataArr.map(function(entry) {
            if (Array.isArray(entry.series)) {
                return entry.series.reduce(function(seriesSum, series) {
                    return seriesSum + (series.buckets || []).reduce(function(bucketSum, bucket) {
                        return bucketSum + toNumber(bucket.cost);
                    }, 0);
                }, 0);
            }
            return (entry.total || []).reduce(function(s, m) {
                return s + sumUsage(m.usage);
            }, 0);
        });
        // Prefer non-zero entry; if multiple non-zero, prefer CNY.
        var pick = null;
        for (var i = 0; i < bizDataArr.length; i++) {
            if (sums[i] > 0) {
                if (!pick) pick = bizDataArr[i];
                else if (bizDataArr[i].currency === 'CNY') pick = bizDataArr[i];
            }
        }
        if (pick) return pick;
        // All zero: prefer CNY entry, else first.
        for (var j = 0; j < bizDataArr.length; j++) {
            if (bizDataArr[j].currency === 'CNY') return bizDataArr[j];
        }
        return bizDataArr[0];
    }
    // ========== Data Processing (called by interceptors) ==========
    function processApiData(bizData) {
        var dataMap = new Map();

        if (bizData && Array.isArray(bizData.series)) {
            bizData.series.forEach(function(series) {
                var name = series.model;
                if (!name) return;

                var model = ensureModelEntry(dataMap, name);
                (series.buckets || []).forEach(function(bucket) {
                    var date = bucketTimeToDate(bucket.time);
                    var hit = getAmount(bucket.usage, 'PROMPT_CACHE_HIT_TOKEN');
                    var miss = getAmount(bucket.usage, 'PROMPT_CACHE_MISS_TOKEN');
                    var daily = model.daily.get(date) || { hit: 0, miss: 0, rate: null };

                    daily.hit += hit;
                    daily.miss += miss;
                    daily.rate = computeRate(daily.hit, daily.miss);
                    model.daily.set(date, daily);

                    model.total.hit += hit;
                    model.total.miss += miss;
                });
                model.total.rate = computeRate(model.total.hit, model.total.miss);
            });

            return dataMap;
        }

        // Cumulative (total) data from the legacy response shape.
        (bizData.total || []).forEach(function(entry) {
            var name = entry.model;
            var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
            var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
            dataMap.set(name, {
                total: { hit: hit, miss: miss, rate: computeRate(hit, miss) },
                daily: new Map()
            });
        });

        // Daily data from the legacy response shape.
        (bizData.days || []).forEach(function(day) {
            var date = day.date;
            (day.data || []).forEach(function(entry) {
                var name = entry.model;
                if (!dataMap.has(name)) return;
                var hit = getAmount(entry.usage, 'PROMPT_CACHE_HIT_TOKEN');
                var miss = getAmount(entry.usage, 'PROMPT_CACHE_MISS_TOKEN');
                dataMap.get(name).daily.set(date, {
                    hit: hit,
                    miss: miss,
                    rate: computeRate(hit, miss)
                });
            });
        });

        return dataMap;
    }

    function processCostData(bizData) {
        var bizDataArr = Array.isArray(bizData) ? bizData : (bizData && bizData.data);
        var entry = pickCurrencyEntry(bizDataArr);
        if (!entry) return null;
        var map = new Map();

        if (Array.isArray(entry.series)) {
            entry.series.forEach(function(series) {
                var name = series.model;
                if (!name) return;
                if (!map.has(name)) map.set(name, { total: 0, daily: new Map() });
                var item = map.get(name);
                (series.buckets || []).forEach(function(bucket) {
                    var date = bucketTimeToDate(bucket.time);
                    var cost = toNumber(bucket.cost);
                    item.total += cost;
                    item.daily.set(date, (item.daily.get(date) || 0) + cost);
                });
            });
            return { data: map, currency: entry.currency || 'CNY' };
        }

        (entry.total || []).forEach(function(m) {
            map.set(m.model, { total: sumUsage(m.usage), daily: new Map() });
        });
        (entry.days || []).forEach(function(day) {
            (day.data || []).forEach(function(m) {
                if (!map.has(m.model)) return;
                map.get(m.model).daily.set(day.date, sumUsage(m.usage));
            });
        });
        return { data: map, currency: entry.currency || 'CNY' };
    }
    function scheduleUIUpdate() {
        clearTimeout(uiUpdateTimer);
        uiUpdateTimer = setTimeout(updateUI, 300);
    }

    function updateUI() {
        if (modelData.size === 0) return;
        clearTimeout(resetTimer);
        resetTimer = null;
        dailyDate = getDefaultDailyDate();
        injectAllCapsules();
    }

    // ========== XHR + fetch Interception ==========
    function handleAmountApiResponse(json) {
        if (!json || json.code !== 0 || !json.data || !json.data.biz_data) return;
        var bizData = json.data.biz_data;
        modelData = processApiData(bizData);
        scheduleUIUpdate();
    }

    function handleCostApiResponse(json) {
        if (!json || json.code !== 0 || !json.data || !json.data.biz_data) return;
        var result = processCostData(json.data.biz_data);
        if (!result) return;
        costData = result.data;
        costCurrency = result.currency;
        scheduleUIUpdate();
    }

    function routeApiResponse(url, json) {
        url = String(url || '');
        if (!url) return;
        if (isAmountUrl(url)) handleAmountApiResponse(json);
        else if (isCostUrl(url)) handleCostApiResponse(json);
    }

    function isAmountUrl(url) {
        return url.indexOf('/usage/amount') !== -1 || url.indexOf('/usage/by_api_key/amount') !== -1;
    }

    function isCostUrl(url) {
        return url.indexOf('/usage/cost') !== -1 || url.indexOf('/usage/by_api_key/cost') !== -1;
    }

    function isUsageUrl(url) {
        return isAmountUrl(url) || isCostUrl(url);
    }

    function getFetchUrl(input) {
        if (typeof input === 'string') return input;
        if (input && typeof input.url === 'string') return input.url;
        return '';
    }
    // --- Intercept XMLHttpRequest ---
    var origSend = XMLHttpRequest.prototype.send;
    var origOpen = XMLHttpRequest.prototype.open;

    XMLHttpRequest.prototype.open = function(method, url) {
        this._ds_url = url;
        return origOpen.apply(this, arguments);
    };

    XMLHttpRequest.prototype.send = function() {
        var self = this;
        var url = self._ds_url;
        if (url && isUsageUrl(String(url))) {
            self.addEventListener('load', function() {
                try {
                    var json = JSON.parse(self.responseText);
                    routeApiResponse(url, json);
                } catch(e) {}
            });
        }
        return origSend.apply(this, arguments);
    };

    // --- Intercept fetch ---
    if (typeof window.fetch !== 'undefined') {
        var origFetch = window.fetch;
        window.fetch = function(input, options) {
            var url = getFetchUrl(input);
            return origFetch.apply(this, arguments).then(function(response) {
                if (isUsageUrl(url)) {
                    var cloned = response.clone();
                    cloned.json().then(function(json) {
                        routeApiResponse(url, json);
                    }).catch(function() {});
                }
                return response;
            });
        };
    }
    // ========== DOM-dependent code (deferred) ==========
    function onDomReady() {
        injectCSS();
        setupGlobalTooltipObserver();
        waitForReact();
    }

    function injectCSS() {
        var s = document.createElement('style');
        s.textContent =
            '.ds-cache-hit-badge{' +
            'display:inline-block;padding:2px 10px;margin-left:8px;' +
            'font-size:12px;line-height:1.5;font-weight:500;' +
            'border-radius:20px;white-space:nowrap;vertical-align:middle;' +
            'transition:background 0.15s ease;' +
            'color:rgb(var(--ds-rgb-primary,0 0 0));' +
            'background:rgb(var(--ds-rgb-elevated,255 255 255));' +
            'border:1px solid rgb(var(--ds-rgb-separator,210 216 229));}' +
            '.ds-cache-hit-badge.cache-na{' +
            'color:rgb(var(--ds-rgb-label-2,144 148 162));border-style:dashed;}' +
            '.ds-cache-hit-badge.cache-high{color:#16A34A;border-color:#16A34A;}' +
            '.ds-cache-hit-badge.cache-mid{color:#D97706;border-color:#D97706;}';
        (document.head || document.documentElement).appendChild(s);
    }

    function waitForReact() {
        var attempts = 0;
        function check() {
            var sections = findTokenSections();
            if (sections.length > 0 || document.querySelector('div[role="tablist"]')) {
                setupMonthWatcher();
                setupPageObserver();
                // If we already have data (interceptor caught the API call), update UI now
                if (modelData.size > 0) {
                    updateUI();
                } else {
                    injectAllCapsules();
                }
                return;
            }
            if (++attempts < 20) {
                setTimeout(check, 500);
            }
        }
        check();
    }
    // ========== DOM Query ==========
    function isRowLikeContainer(row, labelNode) {
        if (!row || !row.children || row.children.length < 2) return false;
        var hasOtherText = false;
        Array.prototype.forEach.call(row.children, function(child) {
            if (hasOtherText || child === labelNode) return;
            if (normalizeText(child.textContent)) hasOtherText = true;
        });
        return hasOtherText;
    }

    function findTokenSections() {
        var sections = [];
        var seen = [];
        var nodes = document.querySelectorAll('span, div');

        Array.prototype.forEach.call(nodes, function(node) {
            if (!node || node.children.length !== 0) return;
            if (normalizeText(node.textContent) !== 'Tokens') return;

            var row = node.parentElement;
            if (!isRowLikeContainer(row, node)) return;
            if (seen.indexOf(row) !== -1) return;

            var card = row.parentElement;
            if (!card) return;

            var echartsDiv = card.querySelector('[_echarts_instance_]');
            if (!echartsDiv) return;

            var section = {
                modelName: null,
                tokenContainer: row,
                echartsDiv: echartsDiv
            };
            if (!resolveModelName(section)) return;

            seen.push(row);
            sections.push(section);
        });

        return sections;
    }
    // ========== Capsule Badge Injection ==========
    function createBadge(text, cssClass) {
        var badge = document.createElement('span');
        badge.className = 'ds-cache-hit-badge ' + cssClass;
        badge.textContent = text;
        return badge;
    }

    function injectCapsule(section, data, dailyDateOverride) {
        var container = section.tokenContainer;
        var modelName = resolveModelName(section);
        if (modelName) section.modelName = modelName;
        // Remove old badges
        var old = container.querySelectorAll('.ds-cache-hit-badge');
        old.forEach(function(el) { el.remove(); });

        // Cumulative capsule
        var rate = data ? data.total.rate : null;
        container.appendChild(createBadge(formatRate(rate), rateClass(rate)));

        // Daily capsule
        var date = dailyDateOverride || dailyDate;
        var dailyRate = null;
        if (data && date) {
            var d = data.daily.get(date);
            if (d) dailyRate = d.rate;
        }
        var dailyText = (dailyRate !== null && dailyRate !== undefined)
            ? dateLabel(date) + ': ' + dailyRate.toFixed(1) + '%'
            : dateLabel(date) + ': N/A';

        // Append daily cost
        var costEntry = modelName ? costData.get(modelName) : null;
        if (costEntry && date) {
            var dailyCost = costEntry.daily.get(date);
            var costStr = formatCost(dailyCost);
            if (costStr !== null) {
                dailyText += ' / ' + currencySymbol(costCurrency) + costStr;
            }
        }
        container.appendChild(createBadge(dailyText, rateClass(dailyRate)));
    }

    function injectAllCapsules() {
        var sections = findTokenSections();
        sections.forEach(function(section) {
            var modelName = resolveModelName(section);
            if (modelName) section.modelName = modelName;
            var data = modelName ? modelData.get(modelName) : null;
            injectCapsule(section, data);
        });
    }

    // ========== Tooltip Date Reading ==========
    function getDateFromTooltip(tooltip) {
        if (!tooltip) return null;
        var m = normalizeText(tooltip.textContent).match(/\d{4}-\d{2}-\d{2}/);
        return m ? m[0] : null;
    }
    // ========== Global Tooltip Observer (reads date → updates daily capsule) ==========
    var tipCheckTimer = null;
    var resetTimer = null;

    function checkAllTooltips() {
        var sections = findTokenSections();
        var foundDate = null;
        sections.forEach(function(section) {
            if (foundDate || !section.echartsDiv) return;
            var tooltips = section.echartsDiv.querySelectorAll('div[style*="absolute"]');
            tooltips.forEach(function(tooltip) {
                if (foundDate) return;
                var cs = window.getComputedStyle(tooltip);
                if (cs.visibility === 'hidden' || cs.opacity === '0') return;
                var d = getDateFromTooltip(tooltip);
                if (d) foundDate = d;
            });
        });

        if (foundDate) {
            clearTimeout(resetTimer);
            resetTimer = null;
            if (foundDate !== dailyDate) {
                dailyDate = foundDate;
                injectAllCapsules();
            }
        } else if (dailyDate && dailyDate !== getDefaultDailyDate()) {
            // Mouse left chart — schedule reset to today after idle period
            if (!resetTimer) {
                resetTimer = setTimeout(function() {
                    resetTimer = null;
                    dailyDate = getDefaultDailyDate();
                    injectAllCapsules();
                }, 1500);
            }
        }
    }

    function setupGlobalTooltipObserver() {
        var root = document.getElementById('root') || document.body;
        if (!root) return;
        var observer = new MutationObserver(function() {
            if (tipCheckTimer) return;
            tipCheckTimer = setTimeout(function() {
                tipCheckTimer = null;
                checkAllTooltips();
            }, 60);
        });
        observer.observe(root, {
            subtree: true,
            attributes: true,
            attributeFilter: ['style']
        });
    }

    // ========== Month Select Watcher ==========
    function setupMonthWatcher() {
        var controls = document.querySelectorAll('select, [role="tablist"]');
        if (!controls || controls.length === 0) return;

        Array.prototype.forEach.call(controls, function(ctrl) {
            var eventName = ctrl.tagName === 'SELECT' ? 'change' : 'click';
            ctrl.addEventListener(eventName, function() {
                setTimeout(function() {
                    if (modelData.size > 0) {
                        setTimeout(function() {
                            injectAllCapsules();
                        }, 500);
                    }
                }, 300);
            });
        });
    }
    // ========== Page Observer (SPA re-render detection) ==========
    function setupPageObserver() {
        var main = document.querySelector('main') || document.getElementById('root');
        if (!main) return;

        var debounceTimer = null;
        new MutationObserver(function() {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(function() {
                injectAllCapsules();
                checkAllTooltips();
            }, 400);
        }).observe(main, { childList: true, subtree: true });
    }

    // ========== Entry ==========
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', onDomReady);
    } else {
        onDomReady();
    }
})();

已经用上了,这下子确实一目了然了。

感谢大佬的分享

1 个赞