DeepSeek Usage Enhancer

在 DeepSeek 用量页面注入今日数据(今日消费/请求数/Token/缓存命中率);自动识别新版与旧版页面布局;图表悬停数字加千分位

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         DeepSeek Usage Enhancer
// @namespace    https://github.com/vlify/DeepSeek-Usage-Enhancer
// @version      1.5.2
// @description  在 DeepSeek 用量页面注入今日数据(今日消费/请求数/Token/缓存命中率);自动识别新版与旧版页面布局;图表悬停数字加千分位
// @author       Jmkwang; vlify
// @license      MIT
// @homepageURL  https://github.com/vlify/DeepSeek-Usage-Enhancer
// @supportURL   https://github.com/vlify/DeepSeek-Usage-Enhancer/issues
// @match        https://platform.deepseek.com/usage*
// @run-at       document-start
// @grant        none
// ==/UserScript==

// ============================================================
// 本脚本为 Jmkwang/DeepSeek-Usage-Enhancer 的修改版 (fork)
// 原项目: https://github.com/Jmkwang/DeepSeek-Usage-Enhancer (MIT)
// 修改者: vlify
// 修改内容: ① 元数据规范化, 为 GreasyFork 托管做准备
//           ② 适配新版平台 (2026-07 改版): 拦截 /usage/by_api_key/amount|cost
//              新接口 (series[].buckets[], 金额为字符串), 同时兼容旧版结构
//           ③ v1.5.0: 新版页面 DOM 注入 (统计行后今日数据条 + 模型今日明细行),
//              自动识别新版/旧版布局, 旧版注入保留为兜底
// 许可证: MIT, 完整文本见仓库 LICENSE 文件
// ============================================================

(function () {
  'use strict';

  // ============================================================
  // 状态
  // ============================================================
  let bearerToken = null;

  let rawUserSummary = null;
  let rawUsageAmount = null;
  let rawUsageCost = null;

  // ============================================================
  // 工具函数
  // ============================================================
  function utcToday() {
    const d = new Date();
    return d.getUTCFullYear() + '-' +
      String(d.getUTCMonth() + 1).padStart(2, '0') + '-' +
      String(d.getUTCDate()).padStart(2, '0');
  }

  function utcYesterday() {
    const d = new Date(Date.now() - 86400000);
    return d.getUTCFullYear() + '-' +
      String(d.getUTCMonth() + 1).padStart(2, '0') + '-' +
      String(d.getUTCDate()).padStart(2, '0');
  }

  function utcMonthYear() {
    const d = new Date();
    return { month: d.getUTCMonth() + 1, year: d.getUTCFullYear() };
  }

  function safeJSON(text) {
    try { return JSON.parse(text); } catch { return null; }
  }

  function log(msg) {
    console.log('[DS Inject] ' + msg);
  }

  // ============================================================
  // biz_data 提取
  // ============================================================
  function extractBizData(json) {
    if (!json) return null;
    let result = null;
    if (json.biz_data) result = json.biz_data;
    else if (json.data && json.data.biz_data) result = json.data.biz_data;
    else if (json.data && Array.isArray(json.data) && json.data.length > 0) result = json.data[0];
    else result = json;
    if (Array.isArray(result) && result.length > 0) return result[0];
    return result;
  }

  function findAuthHeader(req) {
    if (req.headers && typeof req.headers.get === 'function') {
      return req.headers.get('Authorization') || req.headers.get('authorization');
    }
    if (req.headers && typeof req.headers === 'object') {
      return req.headers['Authorization'] || req.headers['authorization'];
    }
    return null;
  }

  // ============================================================
  // 数据变换
  // ============================================================
  function usageArrayToMap(usageArr) {
    if (!Array.isArray(usageArr)) return {};
    const map = {};
    for (const u of usageArr) {
      map[u.type] = Number(u.amount) || 0;
    }
    return map;
  }

  function extractMetricsFromUsage(usageMap) {
    const types = Object.keys(usageMap);
    const lowerMap = {};
    for (const t of types) {
      lowerMap[t.toLowerCase()] = usageMap[t];
    }

    const find = (keywords) => {
      for (const kw of keywords) {
        if (usageMap[kw] !== undefined) return usageMap[kw];
        if (lowerMap[kw.toLowerCase()] !== undefined) return lowerMap[kw.toLowerCase()];
        for (const t of types) {
          if (t.toLowerCase().includes(kw.toLowerCase())) return usageMap[t];
        }
      }
      return 0;
    };

    const cachedInput = find(['PROMPT_CACHE_HIT_TOKEN', 'PROMPT_CACHE_HIT_TOKENS',
      'CACHE_HIT_TOKENS', 'cache_hit_tokens', 'prompt_cache_hit_tokens']);
    const uncachedInput = find(['PROMPT_CACHE_MISS_TOKEN', 'PROMPT_CACHE_MISS_TOKENS',
      'CACHE_MISS_TOKENS', 'cache_miss_tokens', 'prompt_cache_miss_tokens']);
    const output = find(['RESPONSE_TOKEN', 'RESPONSE_TOKENS',
      'COMPLETION_TOKEN', 'COMPLETION_TOKENS',
      'output_tokens', 'completion_tokens']);
    const requests = find(['REQUEST', 'REQUESTS',
      'API_REQUESTS', 'request_count', 'api_requests']);

    const total = cachedInput + uncachedInput + output;
    const divisor = cachedInput + uncachedInput;
    const cacheHitRate = divisor > 0
      ? Math.round((cachedInput / divisor) * 10000) / 100
      : null;

    return { requests, tokens: { total, cached_input: cachedInput, uncached_input: uncachedInput, output }, cache_hit_rate: cacheHitRate };
  }

  // ---- 新版平台: 桶的 time 是 Unix 秒, 按本地时区归入"今日/昨日" ----
  function localDateOf(unixSec) {
    const d = new Date(unixSec * 1000);
    return d.getFullYear() + '-' +
      String(d.getMonth() + 1).padStart(2, '0') + '-' +
      String(d.getDate()).padStart(2, '0');
  }

  function localToday() {
    return localDateOf(Math.floor(Date.now() / 1000));
  }

  function localYesterday() {
    return localDateOf(Math.floor(Date.now() / 1000) - 86400);
  }

  // usage 字段可能是旧版数组 [{type, amount}] 或新版对象 {request, response_token, ...}
  function extractMetricsFromUsageFlexible(usage) {
    if (Array.isArray(usage)) {
      return extractMetricsFromUsage(usageArrayToMap(usage));
    }
    if (usage && typeof usage === 'object') {
      const n = (v) => Number(v) || 0;
      const pick = (...keys) => {
        for (const k of keys) {
          if (usage[k] !== undefined) return n(usage[k]);
        }
        return 0;
      };
      // 真实字段名 (2026-07 新版接口实测):
      // usage = { REQUEST, RESPONSE_TOKEN, PROMPT_CACHE_HIT_TOKEN, PROMPT_CACHE_MISS_TOKEN } (全大写蛇形)
      // 同时兜底 camelCase / snake_case 变体
      const cachedInput = pick('PROMPT_CACHE_HIT_TOKEN', 'promptCacheHitToken', 'prompt_cache_hit_token', 'cached_input_tokens', 'cache_hit_tokens');
      const uncachedInput = pick('PROMPT_CACHE_MISS_TOKEN', 'promptCacheMissToken', 'prompt_cache_miss_token', 'uncached_input_tokens', 'cache_miss_tokens');
      const output = pick('RESPONSE_TOKEN', 'responseToken', 'response_token', 'output_tokens', 'completion_tokens');
      const requests = pick('REQUEST', 'request', 'apiRequests', 'api_requests', 'requests');
      const total = cachedInput + uncachedInput + output;
      const divisor = cachedInput + uncachedInput;
      const cacheHitRate = divisor > 0
        ? Math.round((cachedInput / divisor) * 10000) / 100
        : null;
      return { requests, tokens: { total, cached_input: cachedInput, uncached_input: uncachedInput, output }, cache_hit_rate: cacheHitRate };
    }
    return { requests: 0, tokens: { total: 0, cached_input: 0, uncached_input: 0, output: 0 }, cache_hit_rate: null };
  }

  // 收集 (model, date/time, usage) 记录, 兼容新旧两种结构
  function collectUsageRecords(bizData) {
    const records = [];
    // 新版: series[].buckets[].usage (按模型/API Key 拆分)
    if (Array.isArray(bizData.series)) {
      for (const s of bizData.series) {
        if (!s || !s.model || !Array.isArray(s.buckets)) continue;
        for (const b of s.buckets) {
          if (b && b.usage) records.push({ model: s.model, time: b.time, usage: b.usage });
        }
      }
      return records;
    }
    // 旧版: days[].data[] + total[]
    if (Array.isArray(bizData.days)) {
      for (const day of bizData.days) {
        if (!day || !Array.isArray(day.data)) continue;
        for (const entry of day.data) {
          if (entry && entry.model) records.push({ model: entry.model, date: day.date, usage: entry.usage });
        }
      }
    }
    for (const entry of (bizData.total || [])) {
      if (entry && entry.model) records.push({ model: entry.model, usage: entry.usage });
    }
    return records;
  }

  // 收集 (model, date/time, cost) 记录
  function collectCostRecords(bizData) {
    const records = [];
    // 新版: data[].series[].buckets[].cost (按币种分组, 金额是字符串)
    if (Array.isArray(bizData.data)) {
      for (const group of bizData.data) {
        if (!group || !Array.isArray(group.series)) continue;
        for (const s of group.series) {
          if (!s || !s.model || !Array.isArray(s.buckets)) continue;
          for (const b of s.buckets) {
            if (b) records.push({ model: s.model, time: b.time, cost: Number(b.cost) || 0 });
          }
        }
      }
      return records;
    }
    // 旧版: days[].data[] 的 usage 金额
    if (Array.isArray(bizData.days)) {
      for (const day of bizData.days) {
        if (!day || !Array.isArray(day.data)) continue;
        for (const entry of day.data) {
          if (!entry) continue;
          let c = 0;
          if (Array.isArray(entry.usage)) {
            for (const u of entry.usage) c += Number(u.amount) || 0;
          }
          records.push({ model: entry.model || '', date: day.date, cost: c });
        }
      }
    }
    return records;
  }

  function transformUserSummary(bizData) {
    const normalBal = (bizData.normal_wallets && bizData.normal_wallets[0])
      ? Number(bizData.normal_wallets[0].balance) || 0 : 0;
    const bonusBal = (bizData.bonus_wallets && bizData.bonus_wallets[0])
      ? Number(bizData.bonus_wallets[0].balance) || 0 : 0;

    // 新版可能直接给 total_balance / topped_up_balance / granted_balance
    let total = normalBal + bonusBal;
    if (total === 0) {
      const tb = Number(bizData.total_balance) || 0;
      const tu = Number(bizData.topped_up_balance) || 0;
      const gr = Number(bizData.granted_balance) || 0;
      if (tb > 0) total = tb;
      else if (tu + gr > 0) total = tu + gr;
    }

    const monthlyCost = (bizData.monthly_costs && bizData.monthly_costs[0])
      ? Number(bizData.monthly_costs[0].amount) || 0 : 0;
    const currency = (bizData.monthly_costs && bizData.monthly_costs[0])
      ? bizData.monthly_costs[0].currency : (bizData.currency || 'CNY');

    return {
      balance: { total, normal_wallet_balance: normalBal, bonus_wallet_balance: bonusBal, currency },
      monthly_consumption: { amount: monthlyCost, currency },
    };
  }

  function mergeModelMetrics(target, model, metrics) {
    if (!target[model]) {
      target[model] = metrics;
      return;
    }
    const m = target[model];
    m.requests += metrics.requests;
    m.tokens.total += metrics.tokens.total;
    m.tokens.cached_input += metrics.tokens.cached_input;
    m.tokens.uncached_input += metrics.tokens.uncached_input;
    m.tokens.output += metrics.tokens.output;
    const divisor = m.tokens.cached_input + m.tokens.uncached_input;
    m.cache_hit_rate = divisor > 0
      ? Math.round((m.tokens.cached_input / divisor) * 10000) / 100
      : null;
  }

  // 按日期集合筛选模型数据 (旧版 date 为 UTC 字符串, 新版 time 为 Unix 秒 → 本地日期)
  function extractModelsForDate(bizData, dateStrs) {
    const models = {};
    const dates = new Set(dateStrs);
    for (const rec of collectUsageRecords(bizData)) {
      const recDate = rec.date !== undefined ? rec.date
        : (rec.time !== undefined ? localDateOf(rec.time) : null);
      if (recDate === null || !dates.has(recDate)) continue;
      mergeModelMetrics(models, rec.model, extractMetricsFromUsageFlexible(rec.usage));
    }
    return models;
  }

  function transformUsageAmount(bizData) {
    return {
      today_date: localToday(),
      models: extractModelsForDate(bizData, [utcToday(), localToday()]),
      yesterday_models: extractModelsForDate(bizData, [utcYesterday(), localYesterday()]),
    };
  }

  function transformUsageCost(bizData) {
    let todayCost = 0;
    const modelCosts = {};
    let currency = bizData.currency || 'CNY';
    if (Array.isArray(bizData.data) && bizData.data[0] && bizData.data[0].currency) {
      currency = bizData.data[0].currency;
    }

    for (const rec of collectCostRecords(bizData)) {
      if (rec.date !== undefined) {
        if (rec.date !== utcToday() && rec.date !== localToday()) continue;
      } else if (rec.time !== undefined) {
        if (localDateOf(rec.time) !== localToday()) continue;
      }
      todayCost += rec.cost;
      if (rec.model) modelCosts[rec.model] = (modelCosts[rec.model] || 0) + rec.cost;
    }

    return { today_cost: { amount: Math.floor(todayCost * 100) / 100, currency }, model_costs: modelCosts };
  }

  function buildOutputPayload() {
    const summary = rawUserSummary
      ? transformUserSummary(rawUserSummary)
      : { balance: { total: 0, currency: 'CNY' }, monthly_consumption: { amount: 0, currency: 'CNY' } };

    let usageData = { today_date: utcToday(), models: {}, yesterday_models: {} };
    if (rawUsageAmount) usageData = transformUsageAmount(rawUsageAmount);

    let costData = { today_cost: { amount: 0, currency: 'CNY' } };
    if (rawUsageCost) costData = transformUsageCost(rawUsageCost);

    // 将模型费用合并到模型数据中
    if (costData.model_costs && usageData.models) {
      for (const [model, cost] of Object.entries(costData.model_costs)) {
        if (usageData.models[model]) {
          usageData.models[model].cost = Math.floor(cost * 100) / 100;
        }
      }
    }

    return {
      timestamp: new Date().toISOString(),
      ...summary,
      ...usageData,
      ...costData,
    };
  }

  // ============================================================
  // 数据接收
  // ============================================================
  function processApiResponse(endpoint, bizData) {
    switch (endpoint) {
    case 'get_user_summary': rawUserSummary = bizData; break;
    case 'usage_amount': rawUsageAmount = bizData; break;
    case 'usage_cost': rawUsageCost = bizData; break;
    }
    const payload = buildOutputPayload();
    if (payload) {
      onDataUpdate(payload);
    }
  }

  // ============================================================
  // 拦截层
  // ============================================================
  const TRACKED_ENDPOINTS = [
    { method: 'GET', path: '/users/get_user_summary', id: 'get_user_summary' },
    // 新版平台 (2026-07 改版): 按 API Key 拆分用量
    { method: 'GET', path: '/usage/by_api_key/amount', id: 'usage_amount' },
    { method: 'GET', path: '/usage/by_api_key/cost', id: 'usage_cost' },
    // 旧版平台兼容
    { method: 'GET', path: '/usage/amount', id: 'usage_amount' },
    { method: 'GET', path: '/usage/cost', id: 'usage_cost' },
  ];

  function matchEndpoint(method, url) {
    const m = method.toUpperCase();
    for (const ep of TRACKED_ENDPOINTS) {
      if (m === ep.method && url.includes(ep.path)) {
        return ep;
      }
    }
    // 新版平台也可能通过 /api/v0/users/{id} 返回账户汇总
    if (m === 'GET' && /\/users\/\d+(\/|$|\?)/.test(url)) {
      return { method: 'GET', path: '', id: 'get_user_summary' };
    }
    return null;
  }

  // --- fetch 拦截 ---
  const origFetch = window.fetch;
  window.fetch = function (input, init) {
    const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : '');
    const method = (init && init.method) || (input instanceof Request ? input.method : 'GET');
    const ep = matchEndpoint(method, url);

    if (ep) {
      const req = input instanceof Request ? input : { headers: init && init.headers };
      if (!bearerToken) {
        const auth = findAuthHeader(req);
        if (auth && auth.startsWith('Bearer ')) bearerToken = auth.slice(7);
      }
    }

    return origFetch.call(window, input, init).then(async (response) => {
      if (ep && response.ok) {
        try {
          const cloned = response.clone();
          const json = await cloned.json();
          const bizData = extractBizData(json);
          if (bizData) processApiResponse(ep.id, bizData);
        } catch (e) { /* ignore */ }
      }
      return response;
    });
  };

  // --- XHR 拦截 ---
  const OrigXHR = window.XMLHttpRequest;
  window.XMLHttpRequest = function () {
    const xhr = new OrigXHR();
    let _method = 'GET';
    let _url = '';

    const origOpen = xhr.open;
    xhr.open = function (method, url, ...rest) {
      _method = method;
      _url = typeof url === 'string' ? url : url.toString();
      return origOpen.call(xhr, method, url, ...rest);
    };

    const origSetRequestHeader = xhr.setRequestHeader;
    xhr.setRequestHeader = function (header, value) {
      if (header.toLowerCase() === 'authorization' && value.startsWith('Bearer ')) {
        if (!bearerToken) bearerToken = value.slice(7);
      }
      return origSetRequestHeader.call(xhr, header, value);
    };

    const origSend = xhr.send;
    xhr.send = function (body) {
      const ep = matchEndpoint(_method, _url);
      xhr.addEventListener('load', function () {
        if (ep && xhr.status >= 200 && xhr.status < 300) {
          const json = safeJSON(xhr.responseText);
          if (json) {
            const bizData = extractBizData(json);
            if (bizData) processApiResponse(ep.id, bizData);
          }
        }
      });
      return origSend.call(xhr, body);
    };
    return xhr;
  };
  window.XMLHttpRequest.prototype = OrigXHR.prototype;

  // ============================================================
  // DOM 注入
  // ============================================================
  let latestPayload = null;
  let tooltipObserverSetup = false;
  const INJECT_MARKER = 'data-ds-inject';

  // ============================================================
  // 新版页面 (2026-07 改版) 注入
  // 锚点: 统计行 c7197b0d (消费金额/请求次数/Tokens), 模型名 ce40a39d
  // ============================================================
  const NEW_OVERVIEW_CLS = 'c7197b0d';
  const NEW_MODELNAME_CLS = 'ce40a39d';
  const V2_MARKER = 'data-ds-inject-v2';

  let layout = 'unknown';
  let layoutLogged = false;

  function detectLayout() {
    if (document.querySelector('[class*="' + NEW_OVERVIEW_CLS + '"]') ||
        document.querySelector('[class*="' + NEW_MODELNAME_CLS + '"]')) return 'new';
    if (document.querySelector('[class*="a0cde8c1"]')) return 'old';
    return 'none';
  }

  function ensureLayout() {
    if (layout !== 'new' && layout !== 'old') {
      const d = detectLayout();
      if (d !== 'none') {
        layout = d;
        if (!layoutLogged) { layoutLogged = true; log('页面布局识别: ' + layout); }
      } else if (document.body && document.body.children && document.body.children.length > 0 && !layoutLogged) {
        layoutLogged = true;
        log('未识别页面布局, 跳过 DOM 注入 (数据仍在拦截)');
      }
    }
    return layout;
  }

  function v2FmtShort(n) {
    if (n === undefined || n === null) return '—';
    n = Number(n) || 0;
    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 v2Money(n) {
    return '¥' + (Math.floor((Number(n) || 0) * 100) / 100).toFixed(2);
  }

  function v2Chip(label, value) {
    const span = document.createElement('span');
    span.className = 'dsv2-chip';
    const lbl = document.createElement('b');
    lbl.textContent = label;
    const val = document.createElement('i');
    val.textContent = value;
    span.appendChild(lbl);
    span.appendChild(val);
    return span;
  }

  function v2StyleOnce() {
    if (document.getElementById('dsv2-style')) return;
    const st = document.createElement('style');
    st.id = 'dsv2-style';
    st.textContent =
      '.dsv2-strip{display:flex;flex-wrap:wrap;gap:8px;padding:10px 14px;margin:12px 0 4px;' +
      'background:rgba(30,30,46,.92);border:1px solid rgba(255,255,255,.1);border-radius:12px;' +
      'font-family:-apple-system,\'SF Pro Text\',\'Helvetica Neue\',sans-serif;font-size:12px;color:#cdd6f4;' +
      'box-shadow:0 8px 24px rgba(0,0,0,.35);z-index:50;}' +
      '.dsv2-chip{display:inline-flex;align-items:baseline;gap:6px;padding:4px 10px;' +
      'background:rgba(255,255,255,.06);border-radius:8px;white-space:nowrap;}' +
      '.dsv2-chip b{font-weight:600;color:#6c7086;font-size:11px;}' +
      '.dsv2-chip i{font-style:normal;font-weight:700;color:#cdd6f4;font-variant-numeric:tabular-nums;}';
    (document.head || document.body || document.documentElement).appendChild(st);
  }

  function v2Summary(payload) {
    const models = payload.models || {};
    let req = 0, tok = 0, cached = 0, input = 0;
    for (const name of Object.keys(models)) {
      const m = models[name];
      req += m.requests || 0;
      if (m.tokens) {
        tok += m.tokens.total || 0;
        cached += m.tokens.cached_input || 0;
        input += (m.tokens.cached_input || 0) + (m.tokens.uncached_input || 0);
      }
    }
    const hit = input > 0 ? Math.round((cached / input) * 1000) / 10 : null;
    return { cost: (payload.today_cost || {}).amount || 0, req, tok, hit };
  }

  function injectNewLayout() {
    const payload = latestPayload;
    if (!payload) return;
    v2StyleOnce();
    const s = v2Summary(payload);

    // 1) 统计行后插入今日数据条
    const statsRow = document.querySelector('[class*="' + NEW_OVERVIEW_CLS + '"]');
    if (statsRow) {
      let strip = document.querySelector('[' + V2_MARKER + '="strip"]');
      if (!strip) {
        strip = document.createElement('div');
        strip.className = 'dsv2-strip';
        strip.setAttribute(V2_MARKER, 'strip');
        statsRow.insertAdjacentElement('afterend', strip);
      }
      strip.innerHTML = '';
      strip.appendChild(v2Chip('今日消费', v2Money(s.cost)));
      strip.appendChild(v2Chip('今日请求', v2FmtShort(s.req)));
      strip.appendChild(v2Chip('今日Tokens', v2FmtShort(s.tok)));
      strip.appendChild(v2Chip('缓存命中率', s.hit !== null ? s.hit.toFixed(1) + '%' : '—'));
    }

    // 2) 每个模型名后插入今日明细行
    const modelEls = document.querySelectorAll('[class*="' + NEW_MODELNAME_CLS + '"]');
    for (const el of modelEls) {
      const text = (el.textContent || '').trim();
      if (!text) continue;
      let matchedName = null;
      for (const name of Object.keys(payload.models || {})) {
        if (text.toLowerCase().includes(name.toLowerCase()) ||
            name.toLowerCase().includes(text.toLowerCase())) {
          matchedName = name;
          break;
        }
      }
      if (!matchedName) continue;
      const m = payload.models[matchedName];
      const marker = 'model-' + matchedName.replace(/[^a-z0-9-]/gi, '_');
      let row = document.querySelector('[' + V2_MARKER + '="' + marker + '"]');
      if (!row) {
        row = document.createElement('div');
        row.className = 'dsv2-strip';
        row.setAttribute(V2_MARKER, marker);
        el.insertAdjacentElement('afterend', row);
      }
      row.innerHTML = '';
      const nameChip = document.createElement('span');
      nameChip.className = 'dsv2-chip';
      nameChip.textContent = text;
      row.appendChild(nameChip);
      row.appendChild(v2Chip('今日请求', v2FmtShort(m.requests)));
      row.appendChild(v2Chip('今日Tokens', v2FmtShort(m.tokens ? m.tokens.total : 0)));
      row.appendChild(v2Chip('命中率', (m.cache_hit_rate !== null && m.cache_hit_rate !== undefined)
        ? m.cache_hit_rate.toFixed(1) + '%' : '—'));
      row.appendChild(v2Chip('今日消费', v2Money(m.cost)));
    }
  }

  function onDataUpdate(payload) {
    latestPayload = payload;
    tryAllInjections();
  }

  // 检查页面上是否已有注入标记(被 React 重绘后标记消失)
  function isInjectionPresent(marker) {
    return !!document.querySelector('[' + INJECT_MARKER + '="' + marker + '"]');
  }

  function tryAllInjections() {
    if (!latestPayload) return;

    const l = ensureLayout();

    if (l === 'new') {
      injectNewLayout();
    } else if (l === 'old') {
      // 旧版页面注入:今日消费卡片 / 模型今日行
      if (!isInjectionPresent('cost')) {
        injectTodayCostCard();
      } else {
        updateTodayCostAmount();
      }

      const modelNames = Object.keys(latestPayload.models || {});
      for (const name of modelNames) {
        const marker = 'model-' + name.replace(/[^a-z0-9-]/gi, '_');
        if (!isInjectionPresent(marker)) {
          const yd = (latestPayload.yesterday_models || {})[name];
          injectModelData(name, latestPayload.models[name], yd, marker);
        }
      }
    }

    if (!tooltipObserverSetup) {
      setupTooltipFormatter();
      tooltipObserverSetup = true;
    }
  }

  // ---- 今日消费卡片 ----
  function injectTodayCostCard() {
    // 找到本月消费卡片(确认数据已加载:_7ed1d04 里必须有 ¥ + 数字两个 span)
    const allCards = document.querySelectorAll('[class*="a0cde8c1"]');
    let monthCard = null;
    for (const card of allCards) {
      const titleEl = card.querySelector('[class*="_477051d"]');
      if (titleEl && titleEl.textContent.includes('本月消费')) {
        const valueEl = card.querySelector('[class*="_7ed1d04"]');
        if (valueEl && valueEl.querySelectorAll('span').length >= 2) {
          monthCard = card;
        }
        break;
      }
    }
    if (!monthCard) return;

    // 从原始卡片读取精确的 class 名,从头构建(不用 cloneNode,避免 React 导致子元素丢失)
    const cardClass = monthCard.className;
    const titleClass = monthCard.querySelector('[class*="_477051d"]').className;

    const todayCard = document.createElement('div');
    todayCard.className = cardClass;
    todayCard.setAttribute(INJECT_MARKER, 'cost');

    const titleDiv = document.createElement('div');
    titleDiv.className = titleClass;
    titleDiv.textContent = '今日消费';

    const outerDiv = document.createElement('div');
    outerDiv.className = 'abf3dfef';
    const midDiv = document.createElement('div');
    const bbDiv = document.createElement('div');
    bbDiv.className = '_4bb7bee';

    const v7Div = document.createElement('div');
    v7Div.className = '_7ed1d04';
    const yen = document.createElement('span');
    yen.textContent = '¥';
    const amt = document.createElement('span');
    amt.textContent = (latestPayload.today_cost || {}).amount.toFixed(2);
    v7Div.appendChild(yen);
    v7Div.appendChild(amt);
    bbDiv.appendChild(v7Div);

    const cnyDiv = document.createElement('div');
    cnyDiv.className = '_1ef3557';
    cnyDiv.style.cssText = 'color: rgb(var(--ds-rgb-label-2));';
    cnyDiv.textContent = 'CNY';
    bbDiv.appendChild(cnyDiv);

    midDiv.appendChild(bbDiv);
    outerDiv.appendChild(midDiv);

    todayCard.appendChild(titleDiv);
    todayCard.appendChild(outerDiv);

    monthCard.parentElement.insertBefore(todayCard, monthCard.nextSibling);
  }

  function updateTodayCostAmount() {
    const costCard = document.querySelector('[' + INJECT_MARKER + '="cost"]');
    if (!costCard) return;
    const v7Div = costCard.querySelector('[class*="_7ed1d04"]');
    if (!v7Div) return;
    const spans = v7Div.querySelectorAll('span');
    if (spans.length >= 2) {
      spans[1].textContent = (latestPayload.today_cost || {}).amount.toFixed(2);
    }
  }

  // ---- 模型数据注入 ----
  function injectModelData(modelName, modelMetrics, yesterdayMetrics, marker) {
    // 找到页面上对应的模型名 span
    const modelSpans = document.querySelectorAll('.ds-text.ds-text--monospace');
    let targetSpan = null;
    for (const span of modelSpans) {
      const text = span.textContent.trim();
      const tLower = text.toLowerCase();
      const mLower = modelName.toLowerCase();
      if (tLower.includes(mLower) || mLower.includes(tLower)) {
        targetSpan = span;
        break;
      }
    }
    if (!targetSpan) return;

    // 找到 grid 容器
    const sectionHeader = targetSpan.closest('[class*="_6926780"]') || targetSpan.closest('div');
    let gridContainer = sectionHeader.nextElementSibling;
    while (gridContainer && !gridContainer.querySelector('[class*="columns-2"]')) {
      gridContainer = gridContainer.nextElementSibling;
    }
    if (!gridContainer) return;

    const grid = gridContainer.querySelector('[class*="columns-2"]');
    if (!grid) return;

    const gridItems = grid.querySelectorAll('.ds-grid-item');

    // ---- 列1: 请求次数 ----
    let requestColumn = null;
    for (const item of gridItems) {
      if (item.textContent.includes('API 请求次数') || item.textContent.includes('请求次数')) {
        requestColumn = item;
        break;
      }
    }
    if (requestColumn) {
      injectRequestExtras(requestColumn, modelMetrics.requests,
        yesterdayMetrics ? yesterdayMetrics.requests : 0, marker);
    }

    // ---- 列2: Tokens ----
    let tokenColumn = null;
    for (const item of gridItems) {
      const labels = item.querySelectorAll('.ds-text--lsp');
      for (const lbl of labels) {
        if (lbl.textContent.trim() === 'Tokens') {
          tokenColumn = item;
          break;
        }
      }
      if (tokenColumn) break;
    }
    if (tokenColumn) {
      injectTokenToday(tokenColumn, modelMetrics, marker);
    }
  }

  // 创建一行:label(与 API 请求次数/Tokens 同款)+ value(与数值同款)
  function makeInjectRow(labelText, valueText) {
    const row = document.createElement('div');
    row.className = 'ds-flex';
    row.style.cssText = 'align-items:baseline;gap:12px;';

    const label = document.createElement('span');
    label.className = 'ds-text ds-text--fsp ds-text--lsp';
    label.textContent = labelText;

    const value = document.createElement('span');
    value.className = 'ds-text ds-text--label2';
    value.textContent = valueText;

    row.appendChild(label);
    row.appendChild(value);
    return row;
  }

  function injectRequestExtras(column, todayRequests, yesterdayRequests, marker) {
    // 找到 "API 请求次数" label 的那一行
    const labels = column.querySelectorAll('.ds-text--lsp');
    let labelRow = null;
    for (const lbl of labels) {
      if (lbl.textContent.includes('请求次数')) {
        labelRow = lbl.closest('.ds-flex');
        break;
      }
    }
    if (!labelRow) return;

    const stack = labelRow.parentElement;

    // 昨日请求次数(在今日上方)
    const rowY = makeInjectRow('昨日请求次数', yesterdayRequests.toLocaleString());
    rowY.setAttribute(INJECT_MARKER, marker);
    stack.insertBefore(rowY, labelRow.nextSibling);

    // 今日请求次数
    const rowT = makeInjectRow('今日请求次数', todayRequests.toLocaleString());
    rowT.setAttribute(INJECT_MARKER, marker);
    stack.insertBefore(rowT, rowY.nextSibling);
  }

  function injectTokenToday(column, metrics, marker) {
    // 1. 把 "Tokens" label 改成 "本月总Tokens"
    const labels = column.querySelectorAll('.ds-text--lsp');
    let tokenLabelEl = null;
    for (const lbl of labels) {
      if (lbl.textContent.trim() === 'Tokens') {
        lbl.textContent = '本月总Tokens';
        tokenLabelEl = lbl;
        break;
      }
    }
    if (!tokenLabelEl) return;

    // 找到 token label 那一行 → 垂直 stack
    const tokenRow = tokenLabelEl.closest('.ds-flex');
    if (!tokenRow) return;
    const stack = tokenRow.parentElement;

    // 2. 今日总Tokens
    const totalVal = metrics.tokens ? metrics.tokens.total.toLocaleString() : '0';
    const row1 = makeInjectRow('今日总Tokens', totalVal);
    row1.setAttribute(INJECT_MARKER, marker);

    // 3. 缓存命中率
    const rate = metrics.cache_hit_rate !== null && metrics.cache_hit_rate !== undefined
      ? metrics.cache_hit_rate.toFixed(1) + '%' : '—';
    const row2 = makeInjectRow('今日缓存命中率', rate);
    row2.setAttribute(INJECT_MARKER, marker);

    // 插入到 tokenRow 后面(图表 div 前面)
    let insertAfter = tokenRow;
    stack.insertBefore(row1, insertAfter.nextSibling);
    stack.insertBefore(row2, row1.nextSibling);
  }

  // ---- 图表 tooltip 数字千分位 ----
  function setupTooltipFormatter() {
    const formatNumber = (text) => {
      return text.replace(/\b(\d{4,})\b/g, (_, n) => Number(n).toLocaleString());
    };

    const observer = new MutationObserver((mutations) => {
      for (const m of mutations) {
        for (const node of m.addedNodes) {
          if (node.nodeType !== 1) continue;
          // 处理任何包含大数字的新增元素(ECharts tooltip 内层 div 也覆盖)
          if (/\d{4,}/.test(node.textContent)) {
            formatTooltipText(node, formatNumber);
            node.querySelectorAll('*').forEach(el => {
              if (!el._dsFormatted && /\d{4,}/.test(el.textContent)) {
                formatTooltipText(el, formatNumber);
              }
            });
          }
        }
        if (m.type === 'characterData' && m.target.parentElement) {
          const el = m.target.parentElement;
          if (!el._dsFormatted && /\d{4,}/.test(el.textContent)) {
            formatTooltipText(el, formatNumber);
          }
        }
      }
    });

    observer.observe(document.body, { childList: true, subtree: true, characterData: true });
    log('已启用图表数字千分位格式化');
  }

  function formatTooltipText(root, formatter) {
    if (root._dsFormatted) return;
    root._dsFormatted = true;

    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
    const nodes = [];
    while (walker.nextNode()) nodes.push(walker.currentNode);

    for (const node of nodes) {
      const orig = node.textContent;
      const formatted = formatter(orig);
      if (formatted !== orig) {
        node.textContent = formatted;
      }
    }
  }

  // ============================================================
  // 启动
  // ============================================================
  function init() {
    log('DeepSeek Usage Enhancer 已加载');

    let pollFast = true;
    let pollCount = 0;

    function poll() {
      pollCount++;
      tryAllInjections();

      // 初始阶段每 500ms 快速轮询;稳定后降到 3s
      if (pollFast && pollCount > 20) {
        pollFast = false;
      }
      setTimeout(poll, pollFast ? 500 : 3000);
    }

    // 切换标签页回来时,如果注入标记丢失(React 重绘),立即补注
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'visible') {
        pollCount = 0;
        pollFast = true;
        tryAllInjections();
      }
    });

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', () => {
        setTimeout(poll, 800);
      });
    } else {
      setTimeout(poll, 800);
    }
  }

  init();
})();