DeepSeek Usage Enhancer

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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