CodeBuddy Usage Credit Chart

Add a credit usage chart above CodeBuddy usage details table.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         CodeBuddy Usage Credit Chart
// @namespace    https://www.codebuddy.cn/
// @version      0.2.0
// @description  Add a credit usage chart above CodeBuddy usage details table.
// @license      MIT
// @match        https://www.codebuddy.cn/profile/usage*
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  const SCRIPT_ID = "cbuc-script";
  const STYLE_ID = "cbuc-style";
  const ROOT_ID = "cbuc-root";
  const PAYLOAD_EVENT = "cbuc:payload";
  const BRIDGE_FLAG = "__CBUC_PAGE_BRIDGE__";
  const SCHEDULE_DELAY = 120;
  const DAY_MS = 24 * 60 * 60 * 1000;
  const HOUR_MS = 60 * 60 * 1000;
  const UNKNOWN_MODEL_NAME = "未知模型";
  const MODEL_COLOR_PALETTE = [
    "#6f90d6",
    "#6fb7a7",
    "#d49c74",
    "#cdb37a",
    "#c8849f",
    "#8b92d6",
    "#6ea6c8",
    "#94b06d",
    "#a887c8",
    "#cb8f76",
  ];

  const state = {
    recordPool: new Map(),
    datasets: new Map(),
    hydrationJobs: new Map(),
    mounted: false,
    scheduleTimer: null,
    usageRoot: null,
    chartRoot: null,
    lastSignature: "",
    lastUiFingerprint: "",
    lastDataFingerprint: "",
    lastRenderWidth: 0,
    resizeObserver: null,
    mutationObserver: null,
    globalObserver: null,
    interactionHintUntil: 0,
    tooltipCleanup: null,
    splitByModel: false,
    modelColors: new Map(),
    nextModelColorIndex: 0,
  };

  init();

  function init() {
    injectPageBridge();
    ensureStyles();
    window.addEventListener(PAYLOAD_EVENT, handleBridgePayload, false);
    document.addEventListener("click", handleUserInteraction, true);
    document.addEventListener("change", handleUserInteraction, true);
    document.addEventListener("input", handleUserInteraction, true);
    observeDocument();
    whenReady(() => scheduleSync("init"));
  }

  function injectPageBridge() {
    if (document.documentElement && document.documentElement.hasAttribute(`data-${SCRIPT_ID}`)) {
      return;
    }
    const target = document.documentElement || document.head || document.body;
    if (!target) {
      whenReady(injectPageBridge);
      return;
    }
    const script = document.createElement("script");
    script.setAttribute(`data-${SCRIPT_ID}`, "true");
    script.textContent = `(${pageBridge.toString()})(${JSON.stringify({
      eventName: PAYLOAD_EVENT,
      flag: BRIDGE_FLAG,
    })});`;
    target.appendChild(script);
    script.remove();
    if (document.documentElement) {
      document.documentElement.setAttribute(`data-${SCRIPT_ID}`, "true");
    }
  }

  function pageBridge(config) {
    try {
      if (window[config.flag]) {
        return;
      }
      window[config.flag] = true;

      const shouldInspect = (url, contentType) => {
        if (contentType && /json/i.test(contentType)) {
          return true;
        }
        return /usage|credit|profile|detail|record|consum|bill|request/i.test(url || "");
      };

      const emit = (meta, payload) => {
        try {
          const detail = JSON.stringify({ meta, payload });
          window.dispatchEvent(new CustomEvent(config.eventName, { detail }));
        } catch (error) {
          console.warn("[CBUC] emit failed", error);
        }
      };

      const parseJsonText = (text) => {
        if (!text || typeof text !== "string") {
          return null;
        }
        try {
          return JSON.parse(text);
        } catch (error) {
          return null;
        }
      };

      const normalizeBody = (body) => {
        if (!body) {
          return null;
        }
        if (typeof body === "string") {
          return body;
        }
        if (body instanceof URLSearchParams) {
          return body.toString();
        }
        if (typeof FormData !== "undefined" && body instanceof FormData) {
          const pairs = [];
          body.forEach((value, key) => {
            pairs.push([key, typeof value === "string" ? value : String(value)]);
          });
          return new URLSearchParams(pairs).toString();
        }
        return null;
      };

      const originalFetch = window.fetch;
      if (typeof originalFetch === "function") {
        window.fetch = function patchedFetch(input, init) {
          const url = typeof input === "string" ? input : (input && input.url) || "";
          const method =
            (init && init.method) ||
            (input && typeof input === "object" && input.method) ||
            "GET";
          const requestBody = (init && normalizeBody(init.body)) || null;
          const meta = {
            channel: "fetch",
            url,
            method,
            body: requestBody,
            headers: null,
            timestamp: Date.now(),
          };

          try {
            const rawHeaders =
              (init && init.headers) ||
              (input && typeof input === "object" && input.headers) ||
              null;
            meta.headers = normalizeHeaders(rawHeaders);
          } catch (error) {
            meta.headers = null;
          }

          return originalFetch.apply(this, arguments).then((response) => {
            try {
              const clone = response.clone();
              const contentType =
                (clone.headers && clone.headers.get("content-type")) || "";
              if (!shouldInspect(url, contentType)) {
                return response;
              }

              clone
                .text()
                .then((text) => {
                  const payload = parseJsonText(text);
                  if (payload !== null) {
                    emit(meta, payload);
                  }
                })
                .catch(() => {});
            } catch (error) {
              console.warn("[CBUC] fetch clone failed", error);
            }
            return response;
          });
        };
      }

      const originalOpen = XMLHttpRequest.prototype.open;
      const originalSend = XMLHttpRequest.prototype.send;
      const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;

      XMLHttpRequest.prototype.open = function patchedOpen(method, url) {
        this.__cbucMeta = {
          channel: "xhr",
          url: typeof url === "string" ? url : "",
          method: method || "GET",
          body: null,
          headers: {},
          timestamp: Date.now(),
        };
        return originalOpen.apply(this, arguments);
      };

      XMLHttpRequest.prototype.setRequestHeader = function patchedSetRequestHeader(name, value) {
        if (this.__cbucMeta && name) {
          this.__cbucMeta.headers[String(name)] = String(value);
        }
        return originalSetRequestHeader.apply(this, arguments);
      };

      XMLHttpRequest.prototype.send = function patchedSend(body) {
        if (this.__cbucMeta) {
          this.__cbucMeta.body = normalizeBody(body);
        }
        this.addEventListener(
          "load",
          function onLoad() {
            const meta = this.__cbucMeta;
            if (!meta) {
              return;
            }

            const contentType =
              (typeof this.getResponseHeader === "function" &&
                this.getResponseHeader("content-type")) ||
              "";
            if (!shouldInspect(meta.url, contentType)) {
              return;
            }

            let payload = null;
            if (this.responseType === "json") {
              payload = this.response;
            } else if (!this.responseType || this.responseType === "text") {
              payload = parseJsonText(this.responseText);
            }

            if (payload !== null) {
              emit(meta, payload);
            }
          },
          { once: true }
        );
        return originalSend.apply(this, arguments);
      };

      function normalizeHeaders(headers) {
        if (!headers) {
          return null;
        }
        if (typeof Headers !== "undefined" && headers instanceof Headers) {
          const result = {};
          headers.forEach((value, key) => {
            result[key] = value;
          });
          return result;
        }
        if (Array.isArray(headers)) {
          const result = {};
          headers.forEach((entry) => {
            if (Array.isArray(entry) && entry.length >= 2) {
              result[String(entry[0])] = String(entry[1]);
            }
          });
          return result;
        }
        if (typeof headers === "object") {
          return Object.fromEntries(
            Object.entries(headers).map(([key, value]) => [String(key), String(value)])
          );
        }
        return null;
      }
    } catch (error) {
      console.warn("[CBUC] bridge init failed", error);
    }
  }

  function ensureStyles() {
    if (document.getElementById(STYLE_ID)) {
      return;
    }
    const target = document.head || document.documentElement || document.body;
    if (!target) {
      whenReady(ensureStyles);
      return;
    }
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = `
      #${ROOT_ID} {
        --cbuc-bg: linear-gradient(180deg, rgba(41, 44, 61, 0.98), rgba(33, 36, 52, 0.96));
        --cbuc-border: rgba(124, 131, 175, 0.24);
        --cbuc-panel: rgba(75, 79, 109, 0.2);
        --cbuc-grid: rgba(153, 164, 194, 0.16);
        --cbuc-axis: rgba(255, 255, 255, 0.55);
        --cbuc-muted: rgba(255, 255, 255, 0.62);
        --cbuc-text: #ffffff;
        --cbuc-accent: #6f90d6;
        --cbuc-accent-strong: #8ca5de;
        --cbuc-unknown-model: #7f8799;
        --cbuc-shadow: 0 18px 42px rgba(0, 0, 0, 0.24);
        position: relative;
        margin: 0 0 24px;
        padding: 28px 28px 18px;
        border: 1px solid var(--cbuc-border);
        border-radius: 20px;
        background: var(--cbuc-bg);
        box-shadow: var(--cbuc-shadow);
        overflow: hidden;
      }

      #${ROOT_ID}::before {
        content: "";
        position: absolute;
        inset: -80px auto auto -80px;
        width: 220px;
        height: 220px;
        border-radius: 50%;
        background: radial-gradient(circle, rgba(79, 140, 255, 0.16), rgba(79, 140, 255, 0));
        pointer-events: none;
      }

      #${ROOT_ID} .cbuc-header {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        gap: 20px;
        margin-bottom: 18px;
      }

      #${ROOT_ID} .cbuc-title {
        font-size: 13px;
        line-height: 18px;
        letter-spacing: 0.08em;
        color: var(--cbuc-axis);
        text-transform: uppercase;
      }

      #${ROOT_ID} .cbuc-total {
        margin-top: 6px;
        font-size: clamp(34px, 4.6vw, 56px);
        line-height: 1;
        font-weight: 800;
        color: var(--cbuc-text);
      }

      #${ROOT_ID} .cbuc-summary {
        display: grid;
        justify-items: end;
        gap: 8px;
        min-width: 190px;
      }

      #${ROOT_ID} .cbuc-summary-top {
        display: flex;
        align-items: center;
        gap: 10px;
        flex-wrap: wrap;
        justify-content: flex-end;
      }

      #${ROOT_ID} .cbuc-status {
        padding: 6px 10px;
        border-radius: 999px;
        background: rgba(75, 79, 109, 0.2);
        color: var(--cbuc-muted);
        font-size: 12px;
        line-height: 16px;
        white-space: nowrap;
      }

      #${ROOT_ID} .cbuc-status[data-kind="warning"] {
        color: #ffd38c;
        background: rgba(255, 186, 82, 0.14);
      }

      #${ROOT_ID} .cbuc-meta {
        color: var(--cbuc-muted);
        font-size: 13px;
        line-height: 18px;
        text-align: right;
      }

      #${ROOT_ID} .cbuc-toggle {
        appearance: none;
        border: 1px solid rgba(133, 164, 255, 0.3);
        border-radius: 999px;
        padding: 7px 12px;
        background: rgba(76, 87, 124, 0.18);
        color: rgba(255, 255, 255, 0.82);
        font-size: 12px;
        line-height: 16px;
        font-weight: 600;
        cursor: pointer;
        transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease,
          transform 0.15s ease;
      }

      #${ROOT_ID} .cbuc-toggle:hover:not(:disabled) {
        background: rgba(95, 151, 255, 0.18);
        border-color: rgba(133, 164, 255, 0.52);
        transform: translateY(-1px);
      }

      #${ROOT_ID} .cbuc-toggle[aria-pressed="true"] {
        background: rgba(95, 151, 255, 0.24);
        border-color: rgba(133, 164, 255, 0.72);
        color: #ffffff;
      }

      #${ROOT_ID} .cbuc-toggle:disabled {
        opacity: 0.46;
        cursor: not-allowed;
      }

      #${ROOT_ID} .cbuc-canvas {
        position: relative;
        min-height: 360px;
        border-radius: 18px;
        background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
        overflow: hidden;
      }

      #${ROOT_ID} .cbuc-svg {
        display: block;
        width: 100%;
        height: 360px;
      }

      #${ROOT_ID} .cbuc-legend {
        display: flex;
        align-items: center;
        justify-content: center;
        flex-wrap: wrap;
        gap: 10px 14px;
        margin-top: 14px;
        color: var(--cbuc-muted);
        font-size: 13px;
        line-height: 18px;
      }

      #${ROOT_ID} .cbuc-legend-item {
        display: inline-flex;
        align-items: center;
        gap: 8px;
      }

      #${ROOT_ID} .cbuc-legend-swatch,
      #${ROOT_ID} .cbuc-tooltip-swatch {
        width: 16px;
        height: 16px;
        border-radius: 4px;
        background: linear-gradient(180deg, var(--cbuc-accent), var(--cbuc-accent-strong));
        box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
      }

      #${ROOT_ID} .cbuc-tooltip-swatch {
        width: 10px;
        height: 10px;
        border-radius: 999px;
        flex: 0 0 auto;
      }

      #${ROOT_ID} .cbuc-tooltip {
        position: absolute;
        z-index: 20;
        min-width: 160px;
        max-width: 240px;
        padding: 10px 12px;
        border: 1px solid rgba(131, 176, 255, 0.34);
        border-radius: 12px;
        background: rgba(19, 23, 37, 0.95);
        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.24);
        color: #ffffff;
        font-size: 12px;
        line-height: 17px;
        pointer-events: none;
        opacity: 0;
        transform: translateY(4px);
        transition: opacity 0.14s ease, transform 0.14s ease;
      }

      #${ROOT_ID} .cbuc-tooltip.is-visible {
        opacity: 1;
        transform: translateY(0);
      }

      #${ROOT_ID} .cbuc-tooltip-title {
        color: rgba(255, 255, 255, 0.74);
        margin-bottom: 4px;
      }

      #${ROOT_ID} .cbuc-tooltip-value {
        font-size: 15px;
        line-height: 20px;
        font-weight: 700;
      }

      #${ROOT_ID} .cbuc-tooltip-breakdown {
        margin-top: 8px;
        display: grid;
        gap: 6px;
      }

      #${ROOT_ID} .cbuc-tooltip-row {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr) auto auto;
        align-items: center;
        gap: 8px;
        color: rgba(255, 255, 255, 0.86);
      }

      #${ROOT_ID} .cbuc-tooltip-model {
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
      }

      #${ROOT_ID} .cbuc-tooltip-credit {
        font-variant-numeric: tabular-nums;
      }

      #${ROOT_ID} .cbuc-tooltip-share {
        color: rgba(255, 255, 255, 0.58);
        font-variant-numeric: tabular-nums;
      }

      #${ROOT_ID} .cbuc-cursor-line {
        pointer-events: none;
        stroke: rgba(255, 255, 255, 0.6);
        stroke-width: 1;
        stroke-dasharray: 4 4;
        opacity: 0;
        transition: opacity 0.15s ease;
      }

      #${ROOT_ID} .cbuc-cursor-line.is-visible {
        opacity: 1;
      }

      #${ROOT_ID} rect[data-bar="true"] {
        transition: filter 0.15s ease, opacity 0.15s ease;
      }

      #${ROOT_ID} rect[data-bar="true"].is-highlighted {
        filter: url(#cbucGlow) brightness(1.08) !important;
        opacity: 1 !important;
      }

      #${ROOT_ID} rect[data-bar="true"]:not(.is-highlighted).is-dimmed {
        opacity: 0.5;
      }

      #${ROOT_ID} .cbuc-empty,
      #${ROOT_ID} .cbuc-loading {
        position: absolute;
        inset: 0;
        display: grid;
        place-items: center;
        padding: 24px;
        color: var(--cbuc-muted);
        font-size: 14px;
        line-height: 22px;
        text-align: center;
      }

      #${ROOT_ID} .cbuc-loading::before {
        content: "";
        width: 22px;
        height: 22px;
        margin: 0 auto 10px;
        border-radius: 50%;
        border: 2px solid rgba(255, 255, 255, 0.16);
        border-top-color: var(--cbuc-accent-strong);
        animation: cbuc-spin 0.8s linear infinite;
      }

      @keyframes cbuc-spin {
        to {
          transform: rotate(360deg);
        }
      }

      @media (max-width: 900px) {
        #${ROOT_ID} {
          padding: 22px 18px 16px;
        }

        #${ROOT_ID} .cbuc-header {
          flex-direction: column;
          align-items: stretch;
        }

        #${ROOT_ID} .cbuc-summary {
          justify-items: start;
          min-width: 0;
        }

        #${ROOT_ID} .cbuc-summary-top {
          justify-content: flex-start;
        }

        #${ROOT_ID} .cbuc-meta {
          text-align: left;
        }

        #${ROOT_ID} .cbuc-svg {
          height: 320px;
        }

        #${ROOT_ID} .cbuc-canvas {
          min-height: 320px;
        }
      }
    `;
    target.appendChild(style);
  }

  function observeDocument() {
    if (state.globalObserver) {
      return;
    }
    state.globalObserver = new MutationObserver(() => scheduleSync("dom"));
    const startObserve = () => {
      if (!document.body) {
        return false;
      }
      state.globalObserver.observe(document.body, {
        childList: true,
        subtree: true,
      });
      return true;
    };

    if (!startObserve()) {
      whenReady(startObserve);
    }
  }

  function handleBridgePayload(event) {
    const detail = event.detail;
    if (!detail) {
      return;
    }

    let parsed = null;
    try {
      parsed = typeof detail === "string" ? JSON.parse(detail) : detail;
    } catch (error) {
      return;
    }

    const payload = parsed && parsed.payload;
    const meta = parsed && parsed.meta;
    const extracted = extractUsageRecords(payload);
    if (!extracted || !extracted.records.length) {
      return;
    }

    const uiState = getUiState();
    const signature = resolveSignature(meta, extracted, uiState);
    if (!signature) {
      return;
    }

    upsertDataset(signature, extracted, uiState, meta);
    maybeHydrateDataset(signature, extracted, uiState, meta);
    state.interactionHintUntil = 0;
    scheduleSync("payload");
  }

  function handleUserInteraction(event) {
    const target = event.target;
    if (!(target instanceof Element)) {
      return;
    }

    const usageRoot = findUsageSection();
    if (!usageRoot || !usageRoot.contains(target)) {
      return;
    }

    const toggleButton = target.closest("[data-role='toggle-model-mode']");
    if (toggleButton) {
      if (toggleButton.disabled) {
        return;
      }
      state.splitByModel = !state.splitByModel;
      state.lastUiFingerprint = "";
      scheduleSync("toggle-model-mode", 0);
      return;
    }

    if (
      target.closest(".t-radio-button") ||
      target.closest(".t-pagination") ||
      target.closest(".t-date-range-picker") ||
      target.closest(".t-picker") ||
      target.closest(".t-range-input")
    ) {
      state.interactionHintUntil = Date.now() + 2000;
      renderLoadingState("正在同步最新用量数据...");
      scheduleSync("interaction", 220);
    }
  }

  function whenReady(callback) {
    if (document.readyState === "complete" || document.readyState === "interactive") {
      callback();
      return;
    }
    document.addEventListener("DOMContentLoaded", callback, { once: true });
  }

  function scheduleSync(reason, delay = SCHEDULE_DELAY) {
    window.clearTimeout(state.scheduleTimer);
    state.scheduleTimer = window.setTimeout(() => sync(reason), delay);
  }

  function sync(reason) {
    const usageRoot = findUsageSection();
    if (!usageRoot) {
      cleanupMount();
      return;
    }

    state.usageRoot = usageRoot;
    mountInto(usageRoot);

    const uiState = getUiState();
    const currentSignature = getStateSignature(uiState);
    const source = chooseBestSource(uiState, currentSignature);
    const dataset = state.datasets.get(currentSignature);
    const hydrationJob = state.hydrationJobs.get(currentSignature);
    const cachedRecordCount = countCachedRecordsForUiState(uiState);
    const expectedRecordCount = getExpectedCountForRange(dataset, uiState);
    const uiFingerprint = JSON.stringify({
      signature: currentSignature,
      total: uiState.totalCount,
      rows: uiState.tableRecords.length,
      source: source ? source.kind : "none",
      mode: state.splitByModel ? "model" : "total",
      hydrating: Boolean(hydrationJob && hydrationJob.running),
      cached: cachedRecordCount,
      expected: expectedRecordCount,
      loadingHint: isLoadingHintActive(),
    });

    if (
      state.lastUiFingerprint === uiFingerprint &&
      state.lastDataFingerprint === (source ? source.fingerprint : "none") &&
      reason !== "payload"
    ) {
      return;
    }

    state.lastUiFingerprint = uiFingerprint;
    state.lastDataFingerprint = source ? source.fingerprint : "none";
    state.lastSignature = currentSignature;

    if (
      hydrationJob &&
      hydrationJob.running &&
      (!source || !source.records.length)
    ) {
      const progressText =
        Number.isFinite(expectedRecordCount) && expectedRecordCount > 0
          ? `正在自动抓取完整用量数据(${cachedRecordCount}/${expectedRecordCount})...`
          : "正在自动抓取完整用量数据...";
      renderLoadingState(
        progressText
      );
      return;
    }

    if (source && source.records.length) {
      renderChart(uiState, source);
      return;
    }

    if (isLoadingHintActive()) {
      renderLoadingState("正在同步最新用量数据...");
      return;
    }

    renderEmptyState("当前范围内暂无可用的 Credit 用量数据。");
  }

  function mountInto(usageRoot) {
    const table = usageRoot.querySelector(".t-table");
    if (!table) {
      return;
    }

    let root = usageRoot.querySelector(`#${ROOT_ID}`);
    if (!root) {
      root = document.createElement("section");
      root.id = ROOT_ID;
      root.innerHTML = `
        <div class="cbuc-header">
          <div class="cbuc-overview">
            <div class="cbuc-title">Credit Usage</div>
            <div class="cbuc-total">0</div>
          </div>
          <div class="cbuc-summary">
            <div class="cbuc-summary-top">
              <button class="cbuc-toggle" data-role="toggle-model-mode" type="button" aria-pressed="false">
                按模型划分
              </button>
              <div class="cbuc-status" data-role="status">等待数据</div>
            </div>
            <div class="cbuc-meta" data-role="meta">-</div>
          </div>
        </div>
        <div class="cbuc-canvas" data-role="canvas"></div>
        <div class="cbuc-legend" data-role="legend"></div>
        <div class="cbuc-tooltip" data-role="tooltip">
          <div class="cbuc-tooltip-title"></div>
          <div class="cbuc-tooltip-value"></div>
          <div class="cbuc-tooltip-breakdown"></div>
        </div>
      `;
      usageRoot.insertBefore(root, table);
    } else if (root.nextElementSibling !== table) {
      usageRoot.insertBefore(root, table);
    }

    state.chartRoot = root;
    state.mounted = true;

    if (!state.resizeObserver) {
      state.resizeObserver = new ResizeObserver(() => {
        if (!state.chartRoot) {
          return;
        }
        const width = Math.round(state.chartRoot.clientWidth);
        if (width && width !== state.lastRenderWidth) {
          scheduleSync("resize", 40);
        }
      });
    }

    state.resizeObserver.disconnect();
    state.resizeObserver.observe(root);

    if (!state.mutationObserver) {
      state.mutationObserver = new MutationObserver(() => scheduleSync("usage-root"));
    }
    state.mutationObserver.disconnect();
    state.mutationObserver.observe(usageRoot, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["class", "value", "checked"],
    });
  }

  function cleanupMount() {
    state.usageRoot = null;
    state.chartRoot = null;
    state.mounted = false;
    state.lastUiFingerprint = "";
    state.lastDataFingerprint = "";
    state.lastSignature = "";
    if (state.resizeObserver) {
      state.resizeObserver.disconnect();
    }
    if (state.mutationObserver) {
      state.mutationObserver.disconnect();
    }
  }

  function findUsageSection() {
    const titles = Array.from(document.querySelectorAll("div, h1, h2, h3, span"));
    for (const element of titles) {
      const ownText = getOwnText(element);
      if (!ownText.startsWith("用量明细")) {
        continue;
      }

      let current = element;
      while (current && current !== document.body) {
        if (current.querySelector(".t-table") && current.querySelector(".t-radio-group")) {
          return current;
        }
        current = current.parentElement;
      }
    }
    return null;
  }

  function getUiState() {
    const usageRoot = state.usageRoot || findUsageSection();
    const checkedInput = usageRoot
      ? usageRoot.querySelector(".t-radio-button.t-is-checked input")
      : null;
    const checkedLabel = usageRoot
      ? usageRoot.querySelector(".t-radio-button.t-is-checked .t-radio-button__label")
      : null;
    const rangeKey = ((checkedInput && checkedInput.value) || (checkedLabel && checkedLabel.textContent) || "")
      .trim()
      .toLowerCase();

    const rangeInputs = usageRoot
      ? usageRoot.querySelectorAll(".t-range-input input.t-input__inner")
      : [];
    const startDate = rangeInputs[0] ? normalizeDateOnly(rangeInputs[0].value) : "";
    const endDate = rangeInputs[1] ? normalizeDateOnly(rangeInputs[1].value) : "";
    const unit = determineUnit(rangeKey, startDate, endDate);

    return {
      usageRoot,
      rangeKey,
      startDate,
      endDate,
      unit,
      totalCount: parseTotalCount(usageRoot),
      tableRecords: parseTableRecords(usageRoot),
      now: Date.now(),
    };
  }

  function parseTotalCount(usageRoot) {
    const totalNode = usageRoot && usageRoot.querySelector(".t-pagination__total");
    const text = totalNode ? totalNode.textContent || "" : "";
    const match = text.match(/(\d+)/);
    return match ? Number(match[1]) : null;
  }

  function parseTableRecords(usageRoot) {
    const table = usageRoot && usageRoot.querySelector(".t-table table");
    if (!table) {
      return [];
    }

    const headers = Array.from(table.querySelectorAll("thead th")).map((th) =>
      (th.textContent || "").trim()
    );
    const creditIndex = headers.findIndex((text) => text.includes("积分消耗"));
    const timeIndex = headers.findIndex((text) => text.includes("时间"));
    const requestIdIndex = headers.findIndex((text) => text.toLowerCase().includes("requestid"));
    const modelIndex = headers.findIndex((text) => /(模型|model)/i.test(text));

    if (creditIndex < 0 || timeIndex < 0) {
      return [];
    }

    const rows = Array.from(table.querySelectorAll("tbody tr"));
    return rows
      .map((row, index) => {
        const cells = Array.from(row.children);
        const requestId = requestIdIndex >= 0 ? getCellText(cells[requestIdIndex]) : "";
        const credit = parseNumber(getCellText(cells[creditIndex]));
        const requestTime = getCellText(cells[timeIndex]);
        if (!Number.isFinite(credit) || !parseDateTime(requestTime)) {
          return null;
        }
        return {
          requestId:
            requestId ||
            makeStableId(
              "table",
              `${index}|${requestTime}|${credit}|${getCellText(cells[creditIndex])}|${getCellText(
                cells[timeIndex]
              )}|${modelIndex >= 0 ? getCellText(cells[modelIndex]) : UNKNOWN_MODEL_NAME}`
            ),
          credit,
          requestTime: normalizeDateTime(requestTime),
          modelName:
            modelIndex >= 0 ? normalizeModelName(getCellText(cells[modelIndex])) : UNKNOWN_MODEL_NAME,
        };
      })
      .filter(Boolean);
  }

  function chooseBestSource(uiState, signature) {
    const dataset = state.datasets.get(signature);
    const tableRecords = uiState.tableRecords;
    const cachedRecords = getCachedRecordsForUiState(uiState);
    const expected = getExpectedCountForRange(dataset, uiState);
    const isComplete = Boolean(expected && cachedRecords.length >= expected);
    const isHydrating = Boolean(dataset && dataset.hydrationStatus === "running");
    const tableSource = tableRecords.length
      ? {
          kind: "table",
          records: dedupeRecords(tableRecords),
          label: "当前仅基于已加载表格数据",
          meta: `${tableRecords.length} 条已加载记录`,
          fingerprint: `table:${fingerprintRecords(tableRecords)}`,
        }
      : null;

    const betterThanTable = cachedRecords.length > tableRecords.length;

    if (cachedRecords.length && isComplete) {
      return {
        kind: "cache-full",
        records: cachedRecords,
        label: "缓存命中(已完整)",
        meta: expected ? `已复用 ${cachedRecords.length}/${expected} 条记录` : `${cachedRecords.length} 条缓存记录`,
        fingerprint: `cache-full:${fingerprintRecords(cachedRecords)}`,
      };
    }

    if (betterThanTable) {
      return {
        kind: "cache-partial",
        records: cachedRecords,
        label: isHydrating ? "缓存命中(正在补齐)" : "缓存命中(部分覆盖)",
        meta: expected
          ? `已复用 ${cachedRecords.length}/${expected} 条记录`
          : `已复用 ${cachedRecords.length} 条缓存记录`,
        fingerprint: `cache-partial:${fingerprintRecords(cachedRecords)}`,
      };
    }

    return tableSource;
  }

  function resolveSignature(meta, extracted, uiState) {
    const hinted = extractRangeHint(meta);
    const startDate = hinted.startDate || uiState.startDate || "";
    const endDate = hinted.endDate || uiState.endDate || "";
    const rangeKey = hinted.rangeKey || uiState.rangeKey || "";
    const unit = determineUnit(rangeKey, startDate, endDate);
    return buildSignature(startDate, endDate, rangeKey, unit);
  }

  function upsertDataset(signature, extracted, uiState, meta) {
    const existing = ensureRangeDataset(signature, uiState);
    const requestTemplate = parseRequestTemplate(meta);
    const canTrustUiState = isUiStateForSignature(uiState, signature);

    for (const record of extracted.records) {
      state.recordPool.set(
        record.requestId,
        mergeUsageRecords(state.recordPool.get(record.requestId), record)
      );
      existing.recordIds.add(record.requestId);
    }

    if (Number.isFinite(extracted.total) && extracted.total > 0) {
      existing.totalExpected = Math.max(existing.totalExpected || 0, extracted.total);
    }
    if (canTrustUiState && Number.isFinite(uiState.totalCount) && uiState.totalCount > 0) {
      existing.totalExpected = Math.max(existing.totalExpected || 0, uiState.totalCount);
    }
    if (extracted.isPaginated) {
      existing.sawPagination = true;
    }

    existing.lastSeenAt = Date.now();
    existing.requestTemplate = requestTemplate || existing.requestTemplate || null;
    existing.lastRequestMeta = meta && meta.url ? { url: meta.url, method: meta.method || "GET" } : existing.lastRequestMeta;
    existing.metaHints.push({
      url: meta && meta.url,
      size: extracted.records.length,
      at: Date.now(),
    });
    existing.metaHints = existing.metaHints.slice(-8);
    existing.hydrationStatus = resolveHydrationStatus(existing, canTrustUiState ? uiState : null);

    state.datasets.set(signature, existing);
  }

  function maybeHydrateDataset(signature, extracted, uiState, meta) {
    const dataset = state.datasets.get(signature);
    if (!dataset) {
      return;
    }

    const scopedUiState = isUiStateForSignature(uiState, signature) ? uiState : null;
    const totalExpected = getExpectedCountForRange(dataset, scopedUiState, extracted);
    const cachedCount = getCachedRecordsForDataset(dataset).length;
    dataset.hydrationStatus = resolveHydrationStatus(dataset, scopedUiState, extracted);
    if (!Number.isFinite(totalExpected) || totalExpected <= cachedCount) {
      dataset.hydrationStatus = "complete";
      return;
    }

    const existingJob = state.hydrationJobs.get(signature);
    if (existingJob && existingJob.running) {
      return;
    }

    const plan = derivePaginationPlan(meta, totalExpected);
    if (!plan) {
      dataset.hydrationStatus = cachedCount > 0 ? "partial" : "idle";
      return;
    }

    const job = {
      running: true,
      completed: false,
      plan,
      pages: new Set([String(plan.currentValue)]),
    };
    dataset.hydrationStatus = "running";
    dataset.requestTemplate = plan;
    dataset.lastSeenAt = Date.now();
    state.hydrationJobs.set(signature, job);

    void fetchRemainingPages(signature, job).finally(() => {
      job.running = false;
      job.completed = true;
      const latestDataset = state.datasets.get(signature);
      if (latestDataset) {
        latestDataset.hydrationStatus = resolveHydrationStatus(latestDataset);
      }
      scheduleSync("hydrate");
    });
  }

  function ensureRangeDataset(signature, uiState) {
    const parsedSignature = parseSignature(signature);
    const existing =
      state.datasets.get(signature) ||
      {
        signature,
        createdAt: Date.now(),
        startDate: parsedSignature.startDate,
        endDate: parsedSignature.endDate,
        rangeKey: parsedSignature.rangeKey,
        unit: parsedSignature.unit,
        recordIds: new Set(),
        totalExpected: null,
        sawPagination: false,
        lastSeenAt: 0,
        hydrationStatus: "idle",
        requestTemplate: null,
        lastRequestMeta: null,
        metaHints: [],
      };

    existing.startDate = parsedSignature.startDate || existing.startDate || "";
    existing.endDate = parsedSignature.endDate || existing.endDate || "";
    existing.rangeKey = parsedSignature.rangeKey || existing.rangeKey || "";
    existing.unit = parsedSignature.unit || existing.unit || "day";

    if (uiState && isUiStateForSignature(uiState, signature)) {
      if (Number.isFinite(uiState.totalCount) && uiState.totalCount > 0) {
        existing.totalExpected = Math.max(existing.totalExpected || 0, uiState.totalCount);
      }
    }

    state.datasets.set(signature, existing);
    return existing;
  }

  function getExpectedCountForRange(dataset, uiState, extracted) {
    const candidates = [
      dataset && dataset.totalExpected,
      extracted && extracted.total,
      uiState && uiState.totalCount,
    ].filter((value) => Number.isFinite(value) && value > 0);
    return candidates.length ? Math.max(...candidates) : null;
  }

  function countCachedRecordsForUiState(uiState) {
    return getCachedRecordsForUiState(uiState).length;
  }

  function getCachedRecordsForUiState(uiState) {
    const bounds = getRangeBounds(uiState);
    if (!bounds) {
      return [];
    }
    return getCachedRecordsByBounds(bounds);
  }

  function getCachedRecordsForDataset(dataset) {
    if (!dataset || !dataset.startDate || !dataset.endDate) {
      return [];
    }
    const bounds = getRangeBounds({
      startDate: dataset.startDate,
      endDate: dataset.endDate,
      unit: dataset.unit || "day",
    });
    if (!bounds) {
      return [];
    }
    return getCachedRecordsByBounds(bounds);
  }

  function getCachedRecordsByBounds(bounds) {
    const records = [];
    for (const record of state.recordPool.values()) {
      const time = parseDateTime(record.requestTime);
      if (!time || time < bounds.filterStart || time > bounds.filterEnd) {
        continue;
      }
      records.push(record);
    }
    return records.sort(
      (left, right) => parseDateTime(left.requestTime) - parseDateTime(right.requestTime)
    );
  }

  function resolveHydrationStatus(dataset, uiState, extracted) {
    const expected = getExpectedCountForRange(dataset, uiState, extracted);
    const cachedCount = dataset ? getCachedRecordsForDataset(dataset).length : 0;
    const job = dataset ? state.hydrationJobs.get(dataset.signature) : null;
    if (job && job.running) {
      return "running";
    }
    if (Number.isFinite(expected) && expected > 0 && cachedCount >= expected) {
      return "complete";
    }
    if (cachedCount > 0) {
      return "partial";
    }
    return "idle";
  }

  function parseSignature(signature) {
    const parts = String(signature || "").split("|");
    return {
      startDate: parts[0] && parts[0] !== "na" ? parts[0] : "",
      endDate: parts[1] && parts[1] !== "na" ? parts[1] : "",
      rangeKey: parts[2] && parts[2] !== "custom" ? parts[2] : parts[2] || "",
      unit: parts[3] || "day",
    };
  }

  function isUiStateForSignature(uiState, signature) {
    return Boolean(uiState && signature && getStateSignature(uiState) === signature);
  }

  function derivePaginationPlan(meta, totalExpected) {
    if (!meta || !meta.url) {
      return null;
    }

    const template = parseRequestTemplate(meta);
    if (!template || !Number.isFinite(totalExpected) || totalExpected <= 0) {
      return null;
    }

    if (template.mode === "page") {
      const pageSize = template.pageSize;
      const totalPages = Math.ceil(totalExpected / pageSize);
      if (!Number.isFinite(pageSize) || pageSize <= 0 || totalPages <= 1) {
        return null;
      }
      const firstPage = template.currentValue === 0 ? 0 : 1;
      return {
        ...template,
        totalExpected,
        totalPages,
        firstPage,
      };
    }

    if (template.mode === "offset") {
      const pageSize = template.pageSize;
      const totalPages = Math.ceil(totalExpected / pageSize);
      if (!Number.isFinite(pageSize) || pageSize <= 0 || totalPages <= 1) {
        return null;
      }
      return {
        ...template,
        totalExpected,
        totalPages,
      };
    }

    return null;
  }

  function parseRequestTemplate(meta) {
    let url = null;
    try {
      url = new URL(meta.url, location.origin);
    } catch (error) {
      return null;
    }

    const method = String(meta.method || "GET").toUpperCase();
    const queryParams = Object.fromEntries(url.searchParams.entries());
    const bodyInfo = parseBodyTemplate(meta.body);
    const sources = [];

    if (bodyInfo.params && Object.keys(bodyInfo.params).length) {
      sources.push({
        target: "body",
        params: bodyInfo.params,
        bodyType: bodyInfo.type,
      });
    }

    if (Object.keys(queryParams).length) {
      sources.push({
        target: "query",
        params: queryParams,
        bodyType: bodyInfo.type,
      });
    }

    for (const source of sources) {
      const pageKey = detectNumericKey(source.params, [
        "pageIndex",
        "pageNum",
        "page",
        "current",
        "currentPage",
      ]);
      const sizeKey = detectNumericKey(source.params, [
        "pageSize",
        "page_size",
        "size",
        "limit",
        "pageLimit",
      ], [pageKey]);

      if (pageKey && sizeKey) {
        return {
          mode: "page",
          url,
          method,
          headers: sanitizeHeaders(meta.headers),
          target: source.target,
          bodyType: bodyInfo.type,
          queryParams,
          bodyParams: bodyInfo.params,
          pageKey,
          sizeKey,
          currentValue: Number(source.params[pageKey]),
          pageSize: Number(source.params[sizeKey]),
        };
      }

      const offsetKey = detectNumericKey(source.params, ["offset", "start", "skip"]);
      const limitKey = detectNumericKey(source.params, ["limit", "size", "pageSize", "page_size"], [
        offsetKey,
      ]);

      if (offsetKey && limitKey) {
        return {
          mode: "offset",
          url,
          method,
          headers: sanitizeHeaders(meta.headers),
          target: source.target,
          bodyType: bodyInfo.type,
          queryParams,
          bodyParams: bodyInfo.params,
          offsetKey,
          limitKey,
          currentValue: Number(source.params[offsetKey]),
          pageSize: Number(source.params[limitKey]),
        };
      }
    }

    return null;
  }

  function parseBodyTemplate(body) {
    if (!body || typeof body !== "string") {
      return { type: "none", params: null };
    }

    const trimmed = body.trim();
    if (!trimmed) {
      return { type: "none", params: null };
    }

    if (trimmed.startsWith("{")) {
      try {
        const parsed = JSON.parse(trimmed);
        if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
          return { type: "json", params: parsed };
        }
      } catch (error) {
        return { type: "raw", params: null };
      }
    }

    try {
      const params = Object.fromEntries(new URLSearchParams(trimmed).entries());
      if (Object.keys(params).length) {
        return { type: "form", params };
      }
    } catch (error) {
      return { type: "raw", params: null };
    }

    return { type: "raw", params: null };
  }

  async function fetchRemainingPages(signature, job) {
    const targets = [];
    if (job.plan.mode === "page") {
      for (let index = 0; index < job.plan.totalPages; index += 1) {
        const pageValue = job.plan.firstPage + index;
        if (pageValue !== job.plan.currentValue) {
          targets.push(pageValue);
        }
      }
    } else {
      for (let offset = 0; offset < job.plan.totalExpected; offset += job.plan.pageSize) {
        if (offset !== job.plan.currentValue) {
          targets.push(offset);
        }
      }
    }

    const concurrency = 3;
    let pointer = 0;

    const runner = async () => {
      while (pointer < targets.length) {
        const currentIndex = pointer;
        pointer += 1;
        const targetValue = targets[currentIndex];
        if (job.pages.has(String(targetValue))) {
          continue;
        }
        job.pages.add(String(targetValue));
        try {
          const response = await fetchHydrationPage(job.plan, targetValue);
          const extracted = extractUsageRecords(response);
          if (!extracted || !extracted.records.length) {
            continue;
          }
          upsertDataset(
            signature,
            extracted,
            getUiState(),
            { url: job.plan.url.toString(), method: job.plan.method }
          );
          scheduleSync("hydrate-page");
        } catch (error) {
          console.warn("[CBUC] hydrate page failed", error);
        }
      }
    };

    await Promise.all(Array.from({ length: Math.min(concurrency, targets.length) }, runner));
  }

  async function fetchHydrationPage(plan, targetValue) {
    const request = buildHydrationRequest(plan, targetValue);
    const response = await fetch(request.url, request.options);
    const text = await response.text();
    if (!text) {
      return null;
    }
    return JSON.parse(text);
  }

  function buildHydrationRequest(plan, targetValue) {
    const url = new URL(plan.url.toString());
    const headers = new Headers();
    Object.entries(plan.headers || {}).forEach(([key, value]) => {
      if (!/^(content-length|host|cookie)$/i.test(key)) {
        headers.set(key, value);
      }
    });

    const queryParams = new URLSearchParams(url.search);
    Object.entries(plan.queryParams || {}).forEach(([key, value]) => {
      queryParams.set(key, value);
    });

    let body = null;
    if (plan.mode === "page") {
      if (plan.target === "query") {
        queryParams.set(plan.pageKey, String(targetValue));
      } else {
        body = buildBodyPayload(plan, plan.pageKey, targetValue, headers);
      }
    } else if (plan.target === "query") {
      queryParams.set(plan.offsetKey, String(targetValue));
    } else {
      body = buildBodyPayload(plan, plan.offsetKey, targetValue, headers);
    }

    url.search = queryParams.toString();

    const options = {
      method: plan.method,
      credentials: "include",
      headers,
    };

    if (body != null && plan.method !== "GET" && plan.method !== "HEAD") {
      options.body = body;
    }

    return { url: url.toString(), options };
  }

  function buildBodyPayload(plan, key, value, headers) {
    if (plan.bodyType === "json") {
      const payload = JSON.parse(JSON.stringify(plan.bodyParams || {}));
      payload[key] = value;
      if (!headers.has("content-type")) {
        headers.set("content-type", "application/json;charset=UTF-8");
      }
      return JSON.stringify(payload);
    }

    const params = new URLSearchParams();
    Object.entries(plan.bodyParams || {}).forEach(([name, currentValue]) => {
      params.set(name, String(currentValue));
    });
    params.set(key, String(value));
    if (!headers.has("content-type")) {
      headers.set("content-type", "application/x-www-form-urlencoded;charset=UTF-8");
    }
    return params.toString();
  }

  function detectNumericKey(params, candidates, exclude = []) {
    for (const candidate of candidates) {
      if (exclude.includes(candidate)) {
        continue;
      }
      if (Object.prototype.hasOwnProperty.call(params, candidate)) {
        const numeric = Number(params[candidate]);
        if (Number.isFinite(numeric)) {
          return candidate;
        }
      }
    }
    return null;
  }

  function sanitizeHeaders(headers) {
    if (!headers || typeof headers !== "object") {
      return {};
    }
    const result = {};
    Object.entries(headers).forEach(([key, value]) => {
      if (!/^(content-length|host|cookie)$/i.test(key)) {
        result[key] = String(value);
      }
    });
    return result;
  }

  function extractUsageRecords(payload) {
    const seen = new WeakSet();
    let best = null;

    const visit = (node, parentKey, parentObject) => {
      if (!node || typeof node !== "object") {
        return;
      }
      if (seen.has(node)) {
        return;
      }
      seen.add(node);

      if (Array.isArray(node)) {
        const candidate = scoreCandidateArray(node, parentKey, parentObject);
        if (candidate && (!best || candidate.score > best.score)) {
          best = candidate;
        }
        for (const item of node) {
          visit(item, parentKey, parentObject);
        }
        return;
      }

      for (const [key, value] of Object.entries(node)) {
        visit(value, key, node);
      }
    };

    visit(payload, "", null);
    return best;
  }

  function scoreCandidateArray(array, key, parentObject) {
    if (!array.length || typeof array[0] !== "object" || Array.isArray(array[0])) {
      return null;
    }

    const records = [];
    let fieldHits = 0;
    const sample = array.slice(0, Math.min(array.length, 8));

    for (const item of array) {
      const normalized = normalizeUsageRecord(item);
      if (normalized) {
        records.push(normalized);
      }
    }

    if (!records.length) {
      return null;
    }

    for (const item of sample) {
      const keys = Object.keys(item || {}).map((name) => name.toLowerCase());
      if (keys.some((name) => name === "requestid")) {
        fieldHits += 4;
      }
      if (keys.some((name) => name.includes("credit"))) {
        fieldHits += 4;
      }
      if (keys.some((name) => name.includes("requesttime") || name === "time")) {
        fieldHits += 4;
      }
      if (keys.some((name) => name.includes("model") || name.includes("engine"))) {
        fieldHits += 2;
      }
    }

    const keyBonus = /record|list|item|data|usage|detail/i.test(key || "") ? 6 : 0;
    const total = extractTotal(parentObject);
    const isPaginated = Boolean(
      parentObject &&
        Object.keys(parentObject).some((prop) =>
          /page|pagesize|pageindex|pagenum|offset|limit|hasmore/i.test(prop)
        )
    );

    return {
      records: dedupeRecords(records),
      total,
      isPaginated,
      score: fieldHits + keyBonus + Math.min(records.length, 48),
    };
  }

  function normalizeUsageRecord(record) {
    if (!record || typeof record !== "object") {
      return null;
    }

    const requestId =
      getFirstValue(record, ["requestId", "requestID", "reqId", "id"]) || null;
    const credit = parseNumber(
      getFirstValue(record, ["credit", "credits", "creditCost", "consumeCredit", "cost"])
    );
    const requestTime =
      getFirstValue(record, [
        "requestTime",
        "request_time",
        "time",
        "createdAt",
        "created_at",
        "timestamp",
      ]) || null;
    const modelName = extractModelName(record);

    const normalizedTime = normalizeDateTime(requestTime);
    if (!Number.isFinite(credit) || !normalizedTime) {
      return null;
    }

    return {
      requestId:
        requestId || makeStableId("network", `${normalizedTime}|${credit}|${safeStringify(record)}`),
      credit,
      requestTime: normalizedTime,
      modelName,
    };
  }

  function extractTotal(parentObject) {
    if (!parentObject || typeof parentObject !== "object") {
      return null;
    }
    const candidates = [
      "total",
      "totalCount",
      "count",
      "recordCount",
      "recordsTotal",
      "totalRecords",
    ];
    for (const key of candidates) {
      const value = parentObject[key];
      if (Number.isFinite(Number(value))) {
        return Number(value);
      }
    }
    if (parentObject.pagination && Number.isFinite(Number(parentObject.pagination.total))) {
      return Number(parentObject.pagination.total);
    }
    return null;
  }

  function extractRangeHint(meta) {
    const values = new URLSearchParams();
    try {
      const url = new URL(meta && meta.url ? meta.url : location.href, location.origin);
      url.searchParams.forEach((value, key) => values.append(key, value));
    } catch (error) {
      // ignore
    }

    if (meta && typeof meta.body === "string" && meta.body) {
      const body = meta.body.trim();
      if (body.startsWith("{")) {
        try {
          const parsed = JSON.parse(body);
          flattenObject(parsed).forEach(([key, value]) => values.append(key, String(value)));
        } catch (error) {
          // ignore
        }
      } else {
        new URLSearchParams(body).forEach((value, key) => values.append(key, value));
      }
    }

    let startDate = "";
    let endDate = "";
    let rangeKey = "";

    values.forEach((value, key) => {
      const normalizedKey = key.toLowerCase();
      const normalizedValue = String(value).trim();
      if (!normalizedValue) {
        return;
      }
      if (!rangeKey && /\b(3d|7d|30d)\b/i.test(normalizedValue)) {
        rangeKey = normalizedValue.toLowerCase().match(/\b(3d|7d|30d)\b/i)[1];
      }
      if (!startDate && /(start|from|begin)/i.test(normalizedKey)) {
        startDate = normalizeDateOnly(normalizedValue);
      }
      if (!endDate && /(end|to|finish)/i.test(normalizedKey)) {
        endDate = normalizeDateOnly(normalizedValue);
      }
      if (!rangeKey && /(range|days|period)/i.test(normalizedKey) && /\d+d/i.test(normalizedValue)) {
        rangeKey = normalizedValue.toLowerCase();
      }
    });

    return { startDate, endDate, rangeKey };
  }

  function flattenObject(object, prefix = "", result = []) {
    if (!object || typeof object !== "object") {
      return result;
    }
    Object.entries(object).forEach(([key, value]) => {
      const fullKey = prefix ? `${prefix}.${key}` : key;
      if (value && typeof value === "object" && !Array.isArray(value)) {
        flattenObject(value, fullKey, result);
      } else if (!Array.isArray(value)) {
        result.push([fullKey, value]);
      }
    });
    return result;
  }

  function renderChart(uiState, source) {
    if (!state.chartRoot) {
      return;
    }

    const bucketData = aggregateRecords(source.records, uiState);
    if (!bucketData.buckets.length) {
      renderEmptyState("当前范围内暂无可用的 Credit 用量数据。", source);
      return;
    }

    state.lastRenderWidth = Math.round(state.chartRoot.clientWidth);

    const total = bucketData.total;
    const titleNode = state.chartRoot.querySelector(".cbuc-total");
    const statusNode = state.chartRoot.querySelector("[data-role='status']");
    const metaNode = state.chartRoot.querySelector("[data-role='meta']");
    const canvasNode = state.chartRoot.querySelector("[data-role='canvas']");
    const tooltipNode = state.chartRoot.querySelector("[data-role='tooltip']");
    const legendNode = state.chartRoot.querySelector("[data-role='legend']");

    if (!canvasNode || !titleNode || !statusNode || !metaNode || !tooltipNode || !legendNode) {
      return;
    }

    titleNode.textContent = formatCredit(total);
    statusNode.textContent = source.label;
    statusNode.dataset.kind = source.kind.includes("partial") || source.kind === "table" ? "warning" : "default";
    metaNode.textContent = `${formatRangeSummary(uiState)} · ${source.meta}`;
    renderToggleState(true);
    renderLegend(legendNode, bucketData, state.splitByModel);

    canvasNode.innerHTML = "";
    if (state.tooltipCleanup) {
      state.tooltipCleanup();
      state.tooltipCleanup = null;
    }

    const svg = buildSvgChart(
      bucketData,
      canvasNode.clientWidth || state.chartRoot.clientWidth || 800,
      { splitByModel: state.splitByModel }
    );
    svg.classList.add("cbuc-svg");
    canvasNode.appendChild(svg);
    state.tooltipCleanup = attachTooltip(svg, tooltipNode, { splitByModel: state.splitByModel });
  }

  function buildSvgChart(bucketData, containerWidth, options) {
    const splitByModel = Boolean(options && options.splitByModel);
    const width = Math.max(520, containerWidth || 800);
    const height = width < 720 ? 320 : 360;
    const margin = {
      top: 18,
      right: 18,
      bottom: width < 720 ? 74 : 82,
      left: width < 720 ? 52 : 60,
    };
    const plotWidth = width - margin.left - margin.right;
    const plotHeight = height - margin.top - margin.bottom;
    const scale = getNiceScale(bucketData.maxValue);
    const labelStep = getLabelStep(bucketData.buckets.length, plotWidth, bucketData.unit);
    const barGap = bucketData.buckets.length > 40 ? 2 : 6;
    const barWidth = Math.max(3, plotWidth / bucketData.buckets.length - barGap);

    const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
    svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
    svg.setAttribute("width", "100%");
    svg.setAttribute("height", String(height));
    svg.setAttribute("role", "img");
    svg.setAttribute("aria-label", "CodeBuddy Credit usage chart");

    const defs = document.createElementNS(svg.namespaceURI, "defs");
    defs.innerHTML = `
      <linearGradient id="cbucBarGradient" x1="0" x2="0" y1="0" y2="1">
        <stop offset="0%" stop-color="#7c97d8"></stop>
        <stop offset="100%" stop-color="#6784ca"></stop>
      </linearGradient>
      <filter id="cbucGlow" x="-20%" y="-20%" width="140%" height="140%">
        <feDropShadow dx="0" dy="7" stdDeviation="7" flood-color="rgba(122, 146, 204, 0.16)"></feDropShadow>
      </filter>
    `;
    svg.appendChild(defs);

    const gridGroup = document.createElementNS(svg.namespaceURI, "g");
    const yAxisGroup = document.createElementNS(svg.namespaceURI, "g");
    const xAxisGroup = document.createElementNS(svg.namespaceURI, "g");
    const barsGroup = document.createElementNS(svg.namespaceURI, "g");

    scale.ticks.forEach((tick) => {
      const y = margin.top + plotHeight - (tick / scale.max) * plotHeight;
      const line = document.createElementNS(svg.namespaceURI, "line");
      line.setAttribute("x1", String(margin.left));
      line.setAttribute("x2", String(width - margin.right));
      line.setAttribute("y1", String(y));
      line.setAttribute("y2", String(y));
      line.setAttribute("stroke", "rgba(153, 164, 194, 0.16)");
      line.setAttribute("stroke-width", tick === 0 ? "1.4" : "1");
      gridGroup.appendChild(line);

      const label = document.createElementNS(svg.namespaceURI, "text");
      label.setAttribute("x", String(margin.left - 10));
      label.setAttribute("y", String(y + 4));
      label.setAttribute("text-anchor", "end");
      label.setAttribute("font-size", width < 720 ? "11" : "12");
      label.setAttribute("fill", "rgba(255,255,255,0.55)");
      label.textContent = formatAxisValue(tick);
      yAxisGroup.appendChild(label);
    });

    const cursorLine = document.createElementNS(svg.namespaceURI, "line");
    cursorLine.classList.add("cbuc-cursor-line");
    cursorLine.setAttribute("y1", String(margin.top));
    cursorLine.setAttribute("y2", String(margin.top + plotHeight));
    cursorLine.setAttribute("x1", "0");
    cursorLine.setAttribute("x2", "0");
    svg.appendChild(cursorLine);

    bucketData.buckets.forEach((bucket, index) => {
      const x = margin.left + index * (barWidth + barGap) + barGap / 2;
      const scaledHeight = bucket.value <= 0 ? 0 : Math.max(3, (bucket.value / scale.max) * plotHeight);
      const barTopY = margin.top + plotHeight - scaledHeight;
      let segmentCursorY = margin.top + plotHeight;

      if (splitByModel && bucket.stackSegments.length) {
        let stackedHeight = 0;
        bucket.stackSegments.forEach((segment, segmentIndex) => {
          const segmentHeight =
            segment.value <= 0
              ? 0
              : segmentIndex === bucket.stackSegments.length - 1
                ? Math.max(0, scaledHeight - stackedHeight)
                : (segment.value / bucket.value) * scaledHeight;
          stackedHeight += segmentHeight;
          segmentCursorY -= segmentHeight;
          const rect = document.createElementNS(svg.namespaceURI, "rect");
          rect.setAttribute("x", String(x));
          rect.setAttribute("y", String(segmentCursorY));
          rect.setAttribute("width", String(barWidth));
          rect.setAttribute("height", String(segmentHeight));
          rect.setAttribute("rx", String(segmentIndex === bucket.stackSegments.length - 1 ? Math.min(6, barWidth / 2) : 0));
          rect.setAttribute("fill", segment.color);
          rect.setAttribute("filter", segmentHeight ? "url(#cbucGlow)" : "");
          rect.dataset.bar = "true";
          rect.dataset.bucketIndex = String(index);
          barsGroup.appendChild(rect);
        });
      } else {
        const rect = document.createElementNS(svg.namespaceURI, "rect");
        rect.setAttribute("x", String(x));
        rect.setAttribute("y", String(barTopY));
        rect.setAttribute("width", String(barWidth));
        rect.setAttribute("height", String(scaledHeight));
        rect.setAttribute("rx", String(Math.min(6, barWidth / 2)));
        rect.setAttribute("fill", "url(#cbucBarGradient)");
        rect.setAttribute("filter", scaledHeight ? "url(#cbucGlow)" : "");
        rect.dataset.bar = "true";
        rect.dataset.bucketIndex = String(index);
        barsGroup.appendChild(rect);
      }

      const anchorRect = document.createElementNS(svg.namespaceURI, "rect");
      anchorRect.setAttribute("x", String(x));
      anchorRect.setAttribute("y", String(scaledHeight ? barTopY : margin.top + plotHeight - 3));
      anchorRect.setAttribute("width", String(barWidth));
      anchorRect.setAttribute("height", String(scaledHeight || 3));
      anchorRect.setAttribute("fill", "transparent");
      anchorRect.dataset.index = String(index);
      anchorRect.dataset.tooltipTitle = bucket.tooltipTitle;
      anchorRect.dataset.tooltipValue = formatCredit(bucket.value);
      anchorRect.dataset.tooltipBreakdown = encodeTooltipBreakdown(bucket.tooltipModels);
      barsGroup.appendChild(anchorRect);

      if (index % labelStep === 0 || index === bucketData.buckets.length - 1) {
        const label = document.createElementNS(svg.namespaceURI, "text");
        label.setAttribute("x", String(x + barWidth / 2));
        label.setAttribute("y", String(height - 26));
        label.setAttribute("text-anchor", "middle");
        label.setAttribute("font-size", width < 720 ? "10" : "11");
        label.setAttribute("fill", "rgba(255,255,255,0.55)");
        label.textContent = bucket.axisLabel;
        xAxisGroup.appendChild(label);
      }
    });

    const overlay = document.createElementNS(svg.namespaceURI, "rect");
    overlay.setAttribute("x", String(margin.left));
    overlay.setAttribute("y", String(margin.top));
    overlay.setAttribute("width", String(plotWidth));
    overlay.setAttribute("height", String(plotHeight));
    overlay.setAttribute("fill", "transparent");
    overlay.setAttribute("cursor", "crosshair");
    overlay.dataset.role = "chart-overlay";
    svg.dataset.marginLeft = String(margin.left);
    svg.dataset.marginTop = String(margin.top);
    svg.dataset.barWidth = String(barWidth);
    svg.dataset.barGap = String(barGap);
    svg.dataset.plotWidth = String(plotWidth);
    svg.dataset.plotHeight = String(plotHeight);
    svg.dataset.bucketCount = String(bucketData.buckets.length);

    svg.appendChild(gridGroup);
    svg.appendChild(yAxisGroup);
    svg.appendChild(xAxisGroup);
    svg.appendChild(barsGroup);
    svg.appendChild(overlay);
    return svg;
  }

  function attachTooltip(svg, tooltipNode, options) {
    const anchors = Array.from(svg.querySelectorAll("rect[data-index]"));
    const barRects = Array.from(svg.querySelectorAll("rect[data-bar='true']"));
    if (!anchors.length) {
      tooltipNode.classList.remove("is-visible");
      return null;
    }

    const titleNode = tooltipNode.querySelector(".cbuc-tooltip-title");
    const valueNode = tooltipNode.querySelector(".cbuc-tooltip-value");
    const breakdownNode = tooltipNode.querySelector(".cbuc-tooltip-breakdown");
    if (!titleNode || !valueNode || !breakdownNode) {
      return null;
    }

    const overlay = svg.querySelector("rect[data-role='chart-overlay']");
    const cursorLine = svg.querySelector(".cbuc-cursor-line");
    if (!overlay) {
      return null;
    }

    const marginLeft = parseFloat(svg.dataset.marginLeft);
    const marginTop = parseFloat(svg.dataset.marginTop);
    const barWidth = parseFloat(svg.dataset.barWidth);
    const barGap = parseFloat(svg.dataset.barGap);
    const plotWidth = parseFloat(svg.dataset.plotWidth);
    const bucketCount = parseInt(svg.dataset.bucketCount, 10);

    let currentIndex = -1;
    let isVisible = false;

    const getBucketIndexFromX = (clientX) => {
      const canvas = state.chartRoot.querySelector("[data-role='canvas']");
      if (!canvas) {
        return -1;
      }
      const svgRect = svg.getBoundingClientRect();
      const scaleX = svgRect.width / parseFloat(svg.getAttribute("viewBox").split(" ")[2]);
      const relativeX = (clientX - svgRect.left) / scaleX - marginLeft;
      const index = Math.floor(relativeX / (barWidth + barGap));
      if (index < 0 || index >= bucketCount) {
        return -1;
      }
      return index;
    };

    const show = (index) => {
      if (index < 0 || index >= anchors.length) {
        return;
      }
      const target = anchors[index];
      if (!target) {
        return;
      }

      barRects.forEach((rect) => {
        rect.classList.remove("is-highlighted", "is-dimmed");
        if (rect.dataset.bucketIndex !== String(index)) {
          rect.classList.add("is-dimmed");
        } else {
          rect.classList.add("is-highlighted");
        }
      });

      const canvas = state.chartRoot.querySelector("[data-role='canvas']");
      if (!canvas) {
        return;
      }
      const canvasRect = canvas.getBoundingClientRect();
      const svgRect = svg.getBoundingClientRect();
      const scaleX = svgRect.width / parseFloat(svg.getAttribute("viewBox").split(" ")[2]);
      const tooltipRect = tooltipNode.getBoundingClientRect();

      const barX = marginLeft + index * (barWidth + barGap) + barGap / 2 + barWidth / 2;
      const barCenterX = (svgRect.left + barX * scaleX) - canvasRect.left;

      let x = barCenterX + 12;
      let y = (parseFloat(target.getAttribute("y")) * scaleX) - tooltipRect.height - 12;

      const clampedX = Math.max(8, Math.min(x, canvasRect.width - tooltipRect.width - 8));
      const nextY = y < 8 ? (parseFloat(target.getAttribute("y")) * scaleX) + parseFloat(target.getAttribute("height")) * scaleX + 12 : y;

      tooltipNode.style.left = `${clampedX}px`;
      tooltipNode.style.top = `${nextY}px`;
      titleNode.textContent = target.dataset.tooltipTitle || "";
      valueNode.textContent = `总计 ${target.dataset.tooltipValue || "0"} Credits`;
      breakdownNode.innerHTML = buildTooltipBreakdownHtml(
        decodeTooltipBreakdown(target.dataset.tooltipBreakdown || "")
      );
      tooltipNode.classList.add("is-visible");
      isVisible = true;
    };

    const hide = () => {
      tooltipNode.classList.remove("is-visible");
      cursorLine.classList.remove("is-visible");
      breakdownNode.innerHTML = "";
      barRects.forEach((rect) => {
        rect.classList.remove("is-highlighted", "is-dimmed");
      });
      currentIndex = -1;
      isVisible = false;
    };

    const updateCursorLine = (clientX) => {
      if (!cursorLine) {
        return;
      }
      const svgRect = svg.getBoundingClientRect();
      const scaleX = svgRect.width / parseFloat(svg.getAttribute("viewBox").split(" ")[2]);
      const relativeX = (clientX - svgRect.left) / scaleX;
      cursorLine.setAttribute("x1", String(relativeX));
      cursorLine.setAttribute("x2", String(relativeX));
      cursorLine.classList.add("is-visible");
    };

    const onMove = (event) => {
      const index = getBucketIndexFromX(event.clientX);
      if (index === -1) {
        if (isVisible) {
          hide();
        }
        return;
      }
      if (index !== currentIndex) {
        currentIndex = index;
        show(index);
      }
      updateCursorLine(event.clientX);
    };

    overlay.addEventListener("mousemove", onMove);
    overlay.addEventListener("mouseleave", hide);

    return () => {
      overlay.removeEventListener("mousemove", onMove);
      overlay.removeEventListener("mouseleave", hide);
    };
  }

  function renderLoadingState(text) {
    if (!state.chartRoot) {
      return;
    }
    const titleNode = state.chartRoot.querySelector(".cbuc-total");
    const statusNode = state.chartRoot.querySelector("[data-role='status']");
    const metaNode = state.chartRoot.querySelector("[data-role='meta']");
    const canvasNode = state.chartRoot.querySelector("[data-role='canvas']");
    const legendNode = state.chartRoot.querySelector("[data-role='legend']");
    if (!titleNode || !statusNode || !metaNode || !canvasNode || !legendNode) {
      return;
    }

    titleNode.textContent = "...";
    statusNode.textContent = "加载中";
    statusNode.dataset.kind = "default";
    metaNode.textContent = "等待页面返回最新数据";
    if (state.tooltipCleanup) {
      state.tooltipCleanup();
      state.tooltipCleanup = null;
    }
    canvasNode.innerHTML = `<div class="cbuc-loading">${escapeHtml(text)}</div>`;
    renderToggleState(false);
    renderLegend(legendNode, null, false);
  }

  function renderEmptyState(text, source) {
    if (!state.chartRoot) {
      return;
    }
    const titleNode = state.chartRoot.querySelector(".cbuc-total");
    const statusNode = state.chartRoot.querySelector("[data-role='status']");
    const metaNode = state.chartRoot.querySelector("[data-role='meta']");
    const canvasNode = state.chartRoot.querySelector("[data-role='canvas']");
    const legendNode = state.chartRoot.querySelector("[data-role='legend']");
    if (!titleNode || !statusNode || !metaNode || !canvasNode || !legendNode) {
      return;
    }

    titleNode.textContent = "0";
    statusNode.textContent = source ? source.label : "暂无数据";
    statusNode.dataset.kind = source && source.kind === "network-full" ? "default" : "warning";
    metaNode.textContent = source ? source.meta : "当前范围无记录";
    if (state.tooltipCleanup) {
      state.tooltipCleanup();
      state.tooltipCleanup = null;
    }
    canvasNode.innerHTML = `<div class="cbuc-empty">${escapeHtml(text)}</div>`;
    renderToggleState(false);
    renderLegend(legendNode, null, false);
  }

  function aggregateRecords(records, uiState) {
    const range = getRangeBounds(uiState);
    if (!range) {
      return { buckets: [], maxValue: 0, total: 0, unit: uiState.unit, modelTotals: [] };
    }

    const bucketCount =
      uiState.unit === "hour"
        ? Math.floor((range.lastBucketStart - range.firstBucketStart) / HOUR_MS) + 1
        : Math.floor((range.lastBucketStart - range.firstBucketStart) / DAY_MS) + 1;
    const buckets = [];
    const step = uiState.unit === "hour" ? HOUR_MS : DAY_MS;

    for (let index = 0; index < bucketCount; index += 1) {
      const bucketStart = range.firstBucketStart + index * step;
      buckets.push({
        start: bucketStart,
        end: bucketStart + step - 1,
        value: 0,
        axisLabel: formatAxisLabel(bucketStart, uiState.unit, index),
        tooltipTitle: formatTooltipLabel(bucketStart, uiState.unit),
        models: new Map(),
        stackSegments: [],
        tooltipModels: [],
      });
    }

    const modelTotals = new Map();
    records.forEach((record) => {
      const time = parseDateTime(record.requestTime);
      if (!time || time < range.filterStart || time > range.filterEnd) {
        return;
      }

      const bucketStart =
        uiState.unit === "hour" ? startOfHour(time).getTime() : startOfDay(time).getTime();
      const index = Math.floor((bucketStart - range.firstBucketStart) / step);
      if (index >= 0 && index < buckets.length) {
        const modelName = normalizeModelName(record.modelName);
        buckets[index].value += record.credit;
        buckets[index].models.set(modelName, (buckets[index].models.get(modelName) || 0) + record.credit);
        modelTotals.set(modelName, (modelTotals.get(modelName) || 0) + record.credit);
      }
    });

    const sortedModelTotals = Array.from(modelTotals.entries())
      .filter((entry) => entry[1] > 0)
      .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0], "zh-CN"))
      .map(([name, value]) => ({
        name,
        value,
        color: getModelColor(name),
      }));

    buckets.forEach((bucket) => {
      const tooltipModels = Array.from(bucket.models.entries())
        .filter((entry) => entry[1] > 0)
        .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0], "zh-CN"))
        .map(([name, value]) => ({
          name,
          value,
          share: bucket.value > 0 ? value / bucket.value : 0,
          color: getModelColor(name),
        }));

      const stackSegments = sortedModelTotals
        .map((model) => ({
          name: model.name,
          value: bucket.models.get(model.name) || 0,
          color: model.color,
        }))
        .filter((segment) => segment.value > 0);

      bucket.tooltipModels = tooltipModels;
      bucket.stackSegments = stackSegments;
    });

    const maxValue = Math.max(0, ...buckets.map((bucket) => bucket.value));
    const total = buckets.reduce((sum, bucket) => sum + bucket.value, 0);

    return {
      buckets,
      maxValue,
      total,
      unit: uiState.unit,
      modelTotals: sortedModelTotals,
    };
  }

  function renderToggleState(hasData) {
    if (!state.chartRoot) {
      return;
    }
    const button = state.chartRoot.querySelector("[data-role='toggle-model-mode']");
    if (!(button instanceof HTMLButtonElement)) {
      return;
    }
    button.disabled = !hasData;
    button.setAttribute("aria-pressed", hasData && state.splitByModel ? "true" : "false");
  }

  function renderLegend(legendNode, bucketData, splitByModel) {
    if (!legendNode) {
      return;
    }

    if (splitByModel && bucketData && bucketData.modelTotals.length) {
      legendNode.innerHTML = `
        <span class="cbuc-legend-item">
          <span>Credit 用量</span>
        </span>
        ${bucketData.modelTotals
          .map(
            (model) => `
              <span class="cbuc-legend-item">
                <span class="cbuc-legend-swatch" style="background:${escapeHtml(model.color)}"></span>
                <span>${escapeHtml(model.name)}</span>
              </span>
            `
          )
          .join("")}
      `;
      return;
    }

    legendNode.innerHTML = `
      <span class="cbuc-legend-item">
        <span class="cbuc-legend-swatch"></span>
        <span>Credit 用量</span>
      </span>
    `;
  }

  function encodeTooltipBreakdown(items) {
    try {
      return encodeURIComponent(
        JSON.stringify(
          (items || []).map((item) => ({
            name: item.name,
            value: item.value,
            share: item.share,
            color: item.color,
          }))
        )
      );
    } catch (error) {
      return "";
    }
  }

  function decodeTooltipBreakdown(value) {
    if (!value) {
      return [];
    }
    try {
      const parsed = JSON.parse(decodeURIComponent(value));
      return Array.isArray(parsed) ? parsed : [];
    } catch (error) {
      return [];
    }
  }

  function buildTooltipBreakdownHtml(items) {
    if (!items.length) {
      return "";
    }
    return items
      .map(
        (item) => `
          <div class="cbuc-tooltip-row">
            <span class="cbuc-tooltip-swatch" style="background:${escapeHtml(item.color)}"></span>
            <span class="cbuc-tooltip-model">${escapeHtml(item.name)}</span>
            <span class="cbuc-tooltip-credit">${escapeHtml(formatCredit(item.value))}</span>
            <span class="cbuc-tooltip-share">${escapeHtml(formatPercent(item.share))}</span>
          </div>
        `
      )
      .join("");
  }

  function getModelColor(modelName) {
    const normalized = normalizeModelName(modelName);
    if (normalized === UNKNOWN_MODEL_NAME) {
      return "#7f8799";
    }
    if (state.modelColors.has(normalized)) {
      return state.modelColors.get(normalized);
    }

    let color = "";
    if (state.nextModelColorIndex < MODEL_COLOR_PALETTE.length) {
      color = MODEL_COLOR_PALETTE[state.nextModelColorIndex];
      state.nextModelColorIndex += 1;
    } else {
      const hash = hashText(normalized);
      color = `hsl(${hash % 360} 32% 62%)`;
    }
    state.modelColors.set(normalized, color);
    return color;
  }

  function extractModelName(record) {
    const directKeys = ["model", "modelName", "model_name", "llmModel", "engine", "modelId"];
    for (const key of directKeys) {
      const normalized = normalizeModelValue(record[key]);
      if (normalized) {
        return normalized;
      }
    }

    for (const [key, value] of Object.entries(record)) {
      if (!/(model|engine)/i.test(key)) {
        continue;
      }
      const normalized = normalizeModelValue(value);
      if (normalized) {
        return normalized;
      }
    }

    return UNKNOWN_MODEL_NAME;
  }

  function normalizeModelValue(value) {
    if (value == null || value === "") {
      return "";
    }
    if (typeof value === "string" || typeof value === "number") {
      const normalized = normalizeModelName(String(value));
      return normalized === UNKNOWN_MODEL_NAME ? "" : normalized;
    }
    if (Array.isArray(value)) {
      for (const item of value) {
        const normalized = normalizeModelValue(item);
        if (normalized) {
          return normalized;
        }
      }
      return "";
    }
    if (typeof value === "object") {
      const nested = getFirstValue(value, [
        "name",
        "model",
        "modelName",
        "model_name",
        "id",
        "modelId",
        "engine",
      ]);
      return nested != null ? normalizeModelValue(nested) : "";
    }
    return "";
  }

  function normalizeModelName(value) {
    const text = String(value == null ? "" : value)
      .replace(/\s+/g, " ")
      .trim();
    if (!text || text === "[object Object]") {
      return UNKNOWN_MODEL_NAME;
    }
    return text;
  }

  function getRangeBounds(uiState) {
    if (!uiState.startDate || !uiState.endDate) {
      return null;
    }

    const start = parseDateOnly(uiState.startDate);
    const end = parseDateOnly(uiState.endDate);
    if (!start || !end) {
      return null;
    }

    if (uiState.unit === "hour") {
      const filterStart = startOfDay(start).getTime();
      const filterEnd = endOfDay(end).getTime();
      return {
        filterStart,
        filterEnd,
        firstBucketStart: filterStart,
        lastBucketStart: startOfHour(filterEnd).getTime(),
      };
    }

    return {
      filterStart: startOfDay(start).getTime(),
      filterEnd: endOfDay(end).getTime(),
      firstBucketStart: startOfDay(start).getTime(),
      lastBucketStart: startOfDay(end).getTime(),
    };
  }

  function getStateSignature(uiState) {
    return buildSignature(uiState.startDate, uiState.endDate, uiState.rangeKey, uiState.unit);
  }

  function buildSignature(startDate, endDate, rangeKey, unit) {
    return [startDate || "na", endDate || "na", rangeKey || "custom", unit || "day"].join("|");
  }

  function determineUnit(rangeKey, startDate, endDate) {
    if (rangeKey === "3d") {
      return "hour";
    }
    if (rangeKey === "7d" || rangeKey === "30d") {
      return "day";
    }
    const start = parseDateOnly(startDate);
    const end = parseDateOnly(endDate);
    if (!start || !end) {
      return "day";
    }
    const diffDays = Math.floor((startOfDay(end).getTime() - startOfDay(start).getTime()) / DAY_MS) + 1;
    return diffDays <= 3 ? "hour" : "day";
  }

  function getLabelStep(bucketCount, plotWidth, unit) {
    const minSpacing = unit === "hour" ? 62 : 48;
    return Math.max(1, Math.ceil((bucketCount * minSpacing) / Math.max(plotWidth, 1)));
  }

  function getNiceScale(maxValue) {
    if (!maxValue || maxValue <= 0) {
      return { max: 1, ticks: [0, 0.25, 0.5, 0.75, 1] };
    }

    const roughStep = maxValue / 4;
    const magnitude = Math.pow(10, Math.floor(Math.log10(roughStep)));
    const residual = roughStep / magnitude;
    let niceResidual = 1;

    if (residual > 5) {
      niceResidual = 10;
    } else if (residual > 2) {
      niceResidual = 5;
    } else if (residual > 1) {
      niceResidual = 2;
    }

    const step = niceResidual * magnitude;
    const max = Math.ceil(maxValue / step) * step;
    const ticks = [];
    for (let tick = 0; tick <= max + step * 0.001; tick += step) {
      ticks.push(Number(tick.toFixed(10)));
    }
    return { max, ticks };
  }

  function formatAxisLabel(timestamp, unit, index) {
    const date = new Date(timestamp);
    const monthDay = `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
    if (unit === "hour") {
      return date.getHours() === 0 || index === 0
        ? `${monthDay} ${pad(date.getHours())}:00`
        : `${pad(date.getHours())}:00`;
    }
    return monthDay;
  }

  function formatTooltipLabel(timestamp, unit) {
    const date = new Date(timestamp);
    if (unit === "hour") {
      return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
        date.getHours()
      )}:00`;
    }
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
  }

  function formatAxisValue(value) {
    if (value >= 1000) {
      const fixed = value >= 10000 ? 0 : 1;
      return `${Number((value / 1000).toFixed(fixed))}K`;
    }
    if (value >= 10) {
      return Number(value.toFixed(0)).toString();
    }
    if (value >= 1) {
      return Number(value.toFixed(1)).toString();
    }
    return Number(value.toFixed(2)).toString();
  }

  function formatRangeSummary(uiState) {
    if (!uiState.startDate || !uiState.endDate) {
      return "未识别范围";
    }
    return `${uiState.startDate} ~ ${uiState.endDate} · ${uiState.unit === "hour" ? "按小时" : "按天"}聚合`;
  }

  function isLoadingHintActive() {
    return Date.now() < state.interactionHintUntil;
  }

  function parseDateOnly(value) {
    const normalized = normalizeDateOnly(value);
    if (!normalized) {
      return null;
    }
    const [year, month, day] = normalized.split("-").map(Number);
    const date = new Date(year, month - 1, day);
    return Number.isNaN(date.getTime()) ? null : date;
  }

  function parseDateTime(value) {
    if (!value && value !== 0) {
      return null;
    }
    if (value instanceof Date) {
      return Number.isNaN(value.getTime()) ? null : value.getTime();
    }
    if (typeof value === "number") {
      return value > 1e12 ? value : value * 1000;
    }
    const text = String(value).trim();
    if (!text) {
      return null;
    }
    if (/^\d{10,13}$/.test(text)) {
      const numeric = Number(text);
      return text.length === 13 ? numeric : numeric * 1000;
    }
    const normalized = text.replace(/\//g, "-").replace(" ", "T");
    const date = new Date(normalized);
    return Number.isNaN(date.getTime()) ? null : date.getTime();
  }

  function normalizeDateOnly(value) {
    if (!value) {
      return "";
    }
    const match = String(value).trim().match(/(\d{4})[-/](\d{1,2})[-/](\d{1,2})/);
    if (!match) {
      return "";
    }
    return `${match[1]}-${pad(match[2])}-${pad(match[3])}`;
  }

  function normalizeDateTime(value) {
    const time = parseDateTime(value);
    if (!time) {
      return "";
    }
    const date = new Date(time);
    return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(
      date.getHours()
    )}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
  }

  function formatCredit(value) {
    const number = Number(value) || 0;
    return new Intl.NumberFormat("zh-CN", {
      minimumFractionDigits: number % 1 === 0 ? 0 : 2,
      maximumFractionDigits: 2,
    }).format(number);
  }

  function formatPercent(value) {
    const number = Number(value) || 0;
    return `${(number * 100).toFixed(number * 100 >= 10 ? 0 : 1)}%`;
  }

  function parseNumber(value) {
    if (typeof value === "number") {
      return Number.isFinite(value) ? value : NaN;
    }
    if (value == null) {
      return NaN;
    }
    const text = String(value).replace(/[^\d.-]/g, "");
    const parsed = Number(text);
    return Number.isFinite(parsed) ? parsed : NaN;
  }

  function dedupeRecords(records) {
    const unique = new Map();
    records.forEach((record) => {
      unique.set(record.requestId, mergeUsageRecords(unique.get(record.requestId), record));
    });
    return Array.from(unique.values()).sort(
      (left, right) => parseDateTime(left.requestTime) - parseDateTime(right.requestTime)
    );
  }

  function fingerprintRecords(records) {
    const total = records.reduce((sum, record) => sum + record.credit, 0);
    const first = records[0] ? records[0].requestTime : "na";
    const last = records[records.length - 1] ? records[records.length - 1].requestTime : "na";
    const modelSignature = records
      .slice(0, 12)
      .map((record) => `${record.requestId}:${normalizeModelName(record.modelName)}`)
      .join("|");
    return `${records.length}:${first}:${last}:${total.toFixed(2)}:${hashText(modelSignature)}`;
  }

  function mergeUsageRecords(existingRecord, incomingRecord) {
    if (!existingRecord) {
      return incomingRecord;
    }
    if (!incomingRecord) {
      return existingRecord;
    }
    const existingModel = normalizeModelName(existingRecord.modelName);
    const incomingModel = normalizeModelName(incomingRecord.modelName);
    if (existingModel === UNKNOWN_MODEL_NAME && incomingModel !== UNKNOWN_MODEL_NAME) {
      return { ...existingRecord, ...incomingRecord, modelName: incomingModel };
    }
    if (existingModel !== UNKNOWN_MODEL_NAME && incomingModel === UNKNOWN_MODEL_NAME) {
      return { ...existingRecord, ...incomingRecord, modelName: existingModel };
    }
    return { ...existingRecord, ...incomingRecord, modelName: incomingModel || existingModel };
  }

  function getFirstValue(object, keys) {
    for (const key of keys) {
      if (object[key] != null && object[key] !== "") {
        return object[key];
      }
    }
    return null;
  }

  function getOwnText(element) {
    const textNodes = Array.from(element.childNodes)
      .filter((node) => node.nodeType === Node.TEXT_NODE)
      .map((node) => node.textContent || "");
    return textNodes.join("").trim();
  }

  function getCellText(cell) {
    return cell ? (cell.textContent || "").trim() : "";
  }

  function startOfDay(value) {
    const date = new Date(value);
    date.setHours(0, 0, 0, 0);
    return date;
  }

  function endOfDay(value) {
    const date = new Date(value);
    date.setHours(23, 59, 59, 999);
    return date;
  }

  function startOfHour(value) {
    const date = new Date(value);
    date.setMinutes(0, 0, 0);
    return date;
  }

  function pad(value) {
    return String(value).padStart(2, "0");
  }

  function escapeHtml(text) {
    return String(text)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }

  function safeStringify(value) {
    try {
      return JSON.stringify(value) || "";
    } catch (error) {
      return String(value || "");
    }
  }

  function makeStableId(prefix, raw) {
    const text = `${prefix}:${raw}`;
    const hash = hashText(text);
    return `${prefix}-${(hash >>> 0).toString(16)}`;
  }

  function hashText(text) {
    let hash = 2166136261;
    for (let index = 0; index < text.length; index += 1) {
      hash ^= text.charCodeAt(index);
      hash = Math.imul(hash, 16777619);
    }
    return hash >>> 0;
  }
})();