Codex 周限到底用了多少,其实可以自己查。

AI工具57 次阅读48 分钟

一次性使用代码

(() => {
  "use strict";

  const addStyle = (css) => {
    const style = document.createElement("style");
    style.textContent = css;
    document.head.appendChild(style);
  };

  const CONFIG = {
    USD_PER_CREDIT: 40 / 1000,
  };

  const fmtNum = (n) => {
    if (n >= 1e8) return (n / 1e8).toFixed(2) + "亿";
    if (n >= 1e4) return (n / 1e4).toFixed(1) + "万";
    return Number(n || 0).toLocaleString();
  };

  const n = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);

  const tokenTotal = (obj = {}) =>
    n(obj.text_total_tokens) ||
    n(obj.cached_text_input_tokens) +
      n(obj.uncached_text_input_tokens) +
      n(obj.text_output_tokens);

  function getPageBearerToken() {
    const bootstrapData =
      document.getElementById("client-bootstrap")?.textContent || "";

    return bootstrapData.match(
      /[\w-]{30,}\.[\w-]{30,}\.[\w-]{30,}/,
    )?.[0];
  }

  document.getElementById("codex-compass-btn")?.remove();
  document.getElementById("codex-compass-root")?.remove();

  addStyle(`
    #codex-compass-root {
      position: fixed; top: 5%; left: 50%; transform: translateX(-50%);
      width: 720px; max-height: 90vh; overflow: auto; background: #fff;
      border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.3);
      z-index: 2147483647; padding: 24px; display: none;
      flex-direction: column; border: 1px solid #e5e5e5; color: #333;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    }
    .compass-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
    .compass-title { font-size: 18px; font-weight: 600; }
    .compass-close { cursor: pointer; font-size: 28px; color: #999; line-height: 1; }
    .compass-note { font-size: 12px; color: #777; line-height: 1.6; margin: -4px 0 16px; }
    .compass-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
    .compass-card { background: #f9f9f9; padding: 12px; border-radius: 8px; border: 1px solid #eee; }
    .compass-card.highlight { background: #eefaf5; border-color: #d1f2e1; }
    .card-label { font-size: 12px; color: #666; margin-bottom: 4px; }
    .card-value { font-size: 15px; font-weight: 700; color: #10a37f; white-space: nowrap; }
    .table-container { max-height: 220px; overflow-y: auto; border: 1px solid #eee; border-radius: 6px; margin-bottom: 15px; }
    .compass-table { width: 100%; border-collapse: collapse; font-size: 12px; }
    .compass-table thead { position: sticky; top: 0; background: #f5f5f5; z-index: 1; }
    .compass-table th { text-align: left; padding: 8px; border-bottom: 1px solid #eee; color: #666; }
    .compass-table td { padding: 8px; border-bottom: 1px solid #f0f0f0; }
    .compass-footer-row { background: #fafafa; font-weight: 700; position: sticky; bottom: 0; border-top: 2px solid #eee; }
    #codex-compass-btn {
      position: fixed; bottom: 20px; right: 20px; z-index: 2147483646;
      padding: 10px 20px; background: #10a37f; color: white;
      border: none; border-radius: 8px; cursor: pointer; font-weight: 600;
    }
  `);

  async function apiGet(path, token) {
    if (!token) {
      throw new Error("未找到页面 Bearer token,无法访问 Codex usage 接口。");
    }

    const res = await fetch(path, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      credentials: "omit",
    });

    const text = await res.text();

    if (!res.ok) {
      throw new Error(`${path} HTTP ${res.status}: ${text.slice(0, 300)}`);
    }

    try {
      return JSON.parse(text);
    } catch {
      throw new Error(`${path} returned non-JSON: ${text.slice(0, 300)}`);
    }
  }

  function getStats(list) {
    const credits = list.reduce((sum, d) => sum + n(d.totals?.credits), 0);
    const turns = list.reduce((sum, d) => sum + n(d.totals?.turns), 0);
    const tokens = list.reduce((sum, d) => sum + tokenTotal(d.totals), 0);
    return { credits, turns, tokens, usd: credits * CONFIG.USD_PER_CREDIT };
  }

  function renderTable(list, stats) {
    return `
      <div class="table-container">
        <table class="compass-table">
          <thead>
            <tr><th>日期</th><th>Credits</th><th>Tokens</th><th>金额</th><th>轮数</th></tr>
          </thead>
          <tbody>
            ${[...list].reverse().map((row) => `
              <tr>
                <td>${row.date}</td>
                <td style="font-family:monospace">${n(row.totals?.credits).toFixed(3)}</td>
                <td style="font-family:monospace">${fmtNum(tokenTotal(row.totals))}</td>
                <td>$ ${(n(row.totals?.credits) * CONFIG.USD_PER_CREDIT).toFixed(2)}</td>
                <td>${n(row.totals?.turns)}</td>
              </tr>
            `).join("")}
          </tbody>
          <tfoot>
            <tr class="compass-footer-row">
              <td>合计</td>
              <td>${stats.credits.toFixed(3)}</td>
              <td style="font-family:monospace">${fmtNum(stats.tokens)}</td>
              <td style="color:#10a37f">$ ${stats.usd.toFixed(2)}</td>
              <td>${stats.turns}</td>
            </tr>
          </tfoot>
        </table>
      </div>
    `;
  }

  function showPanel(data) {
    const root = document.getElementById("codex-compass-root");
    const { secondary, dailyList, cycleStartDate } = data;

    const currentCycleList = [];
    const historyList = [];

    dailyList.forEach((item) => {
      if (new Date(item.date) >= new Date(cycleStartDate)) {
        currentCycleList.push(item);
      } else {
        historyList.push(item);
      }
    });

    const currentStats = getStats(currentCycleList);
    const historyStats = getStats(historyList);

    const ratio = n(secondary?.used_percent) / 100;
    const estCredits = ratio > 0 ? currentStats.credits / ratio : 0;

    let historyRangeTitle = "历史记录(本周期外)";
    if (historyList.length > 0) {
      const sortedHistory = [...historyList].sort(
        (a, b) => new Date(a.date) - new Date(b.date),
      );
      historyRangeTitle =
        `历史记录(本周期外 ${sortedHistory[0].date} 至 ${sortedHistory.at(-1).date})`;
    }

    root.innerHTML = `
      <div class="compass-header">
        <div class="compass-title">Codex 配额分析</div>
        <div class="compass-close" id="compass-close-btn">&times;</div>
      </div>

      <div class="compass-note">
        说明:analytics 可能延迟几小时;周期切换通常按美西时间计算。脚本会读取当前 chatgpt.com 页面内已有的 Bearer token,仅用于请求 chatgpt.com 同域 usage / analytics 接口;不读取 cookie 内容,不打印、不保存、不上传 token。
      </div>

      <div class="compass-grid">
        <div class="compass-card">
          <div class="card-label">已用比例</div>
          <div class="card-value">${n(secondary?.used_percent)}%</div>
        </div>
        <div class="compass-card">
          <div class="card-label">本周期已用</div>
          <div class="card-value">${currentStats.credits.toFixed(1)} Credits</div>
        </div>
        <div class="compass-card highlight">
          <div class="card-label">推算总额</div>
          <div class="card-value">${estCredits.toFixed(1)} Credits</div>
        </div>
        <div class="compass-card">
          <div class="card-label">周期价值估算</div>
          <div class="card-value">$ ${(estCredits * CONFIG.USD_PER_CREDIT).toFixed(2)}</div>
        </div>
      </div>

      <div style="font-weight:600; margin-bottom:8px; font-size:13px;">本周期明细(始于 ${cycleStartDate})</div>
      ${renderTable(currentCycleList, currentStats)}

      ${
        historyList.length > 0
          ? `<div style="font-weight:600; margin-bottom:8px; font-size:13px; color:#666;">${historyRangeTitle}</div>
             ${renderTable(historyList, historyStats)}`
          : ""
      }
    `;

    root.style.display = "flex";
    document.getElementById("compass-close-btn").onclick = () => {
      root.style.display = "none";
    };
  }

  async function run() {
    const btn = document.getElementById("codex-compass-btn");

    btn.innerText = "分析中...";

    try {
      const token = getPageBearerToken();

      const usage = await apiGet("/backend-api/wham/usage", token);

      const secondary =
        usage?.rate_limit?.secondary_window ||
        usage?.rate_limit?.primary_window;

      const endDate = new Date(Date.now() + 86400000)
        .toISOString()
        .split("T")[0];

      const startDate = new Date(Date.now() - 30 * 86400000)
        .toISOString()
        .split("T")[0];

      const cycleStartDate = secondary
        ? new Date((secondary.reset_at - secondary.limit_window_seconds) * 1000)
            .toISOString()
            .split("T")[0]
        : startDate;

      const dailyData = await apiGet(
        `/backend-api/wham/analytics/daily-workspace-usage-counts?start_date=${startDate}&end_date=${endDate}&group_by=day`,
        token,
      );

      showPanel({
        secondary,
        dailyList: dailyData.data || [],
        cycleStartDate,
      });
    } catch (e) {
      console.error("[Codex Compass]", e);
      alert("错误: " + e.message);
    } finally {
      btn.innerText = "运行用量分析";
    }
  }

  const btn = document.createElement("button");
  btn.id = "codex-compass-btn";
  btn.innerText = "运行用量分析";
  btn.onclick = run;
  document.body.appendChild(btn);

  const root = document.createElement("div");
  root.id = "codex-compass-root";
  document.body.appendChild(root);
})();

油猴脚本代码

// ==UserScript==
// @name         Codex Quota Compass V2.0
// @namespace    https://chatgpt.com/
// @version      2.0
// @description  展示 Codex 配额、Credits、Tokens、周期估算与历史用量
// @match        https://chatgpt.com/*
// @run-at       document-idle
// @grant        GM_addStyle
// ==/UserScript==

(() => {
  "use strict";

  const CONFIG = {
    USD_PER_CREDIT: 40 / 1000,
  };

  const TARGET_PATH = "/codex/cloud/settings/analytics";

  const fmtNum = (n) => {
    if (n >= 1e8) return (n / 1e8).toFixed(2) + "亿";
    if (n >= 1e4) return (n / 1e4).toFixed(1) + "万";
    return Number(n || 0).toLocaleString();
  };

  const n = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);

  const tokenTotal = (obj = {}) =>
    n(obj.text_total_tokens) ||
    n(obj.cached_text_input_tokens) +
      n(obj.uncached_text_input_tokens) +
      n(obj.text_output_tokens);

  function getPageBearerToken() {
    const bootstrapData =
      document.getElementById("client-bootstrap")?.textContent || "";

    return bootstrapData.match(
      /[\w-]{30,}\.[\w-]{30,}\.[\w-]{30,}/,
    )?.[0];
  }

  GM_addStyle(`
    #codex-compass-root {
      position: fixed; top: 5%; left: 50%; transform: translateX(-50%);
      width: 720px; max-height: 90vh; overflow: auto; background: #fff;
      border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.3);
      z-index: 2147483647; padding: 24px; display: none;
      flex-direction: column; border: 1px solid #e5e5e5; color: #333;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    }
    .compass-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
    .compass-title { font-size: 18px; font-weight: 600; }
    .compass-close { cursor: pointer; font-size: 28px; color: #999; line-height: 1; }
    .compass-note { font-size: 12px; color: #777; line-height: 1.6; margin: -4px 0 16px; }
    .compass-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
    .compass-card { background: #f9f9f9; padding: 12px; border-radius: 8px; border: 1px solid #eee; }
    .compass-card.highlight { background: #eefaf5; border-color: #d1f2e1; }
    .card-label { font-size: 12px; color: #666; margin-bottom: 4px; }
    .card-value { font-size: 15px; font-weight: 700; color: #10a37f; white-space: nowrap; }
    .table-container { max-height: 220px; overflow-y: auto; border: 1px solid #eee; border-radius: 6px; margin-bottom: 15px; }
    .compass-table { width: 100%; border-collapse: collapse; font-size: 12px; }
    .compass-table thead { position: sticky; top: 0; background: #f5f5f5; z-index: 1; }
    .compass-table th { text-align: left; padding: 8px; border-bottom: 1px solid #eee; color: #666; }
    .compass-table td { padding: 8px; border-bottom: 1px solid #f0f0f0; }
    .compass-footer-row { background: #fafafa; font-weight: 700; position: sticky; bottom: 0; border-top: 2px solid #eee; }
    #codex-compass-btn {
      position: fixed; bottom: 20px; right: 20px; z-index: 2147483646;
      padding: 10px 20px; background: #10a37f; color: white;
      border: none; border-radius: 8px; cursor: pointer; font-weight: 600;
    }
  `);

  async function apiGet(path, token) {
    if (!token) {
      throw new Error("未找到页面 Bearer token,无法访问 Codex usage 接口。");
    }

    const res = await fetch(path, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      credentials: "omit",
    });

    const text = await res.text();

    if (!res.ok) {
      throw new Error(`${path} HTTP ${res.status}: ${text.slice(0, 300)}`);
    }

    try {
      return JSON.parse(text);
    } catch {
      throw new Error(`${path} returned non-JSON: ${text.slice(0, 300)}`);
    }
  }

  function getStats(list) {
    const credits = list.reduce((sum, d) => sum + n(d.totals?.credits), 0);
    const turns = list.reduce((sum, d) => sum + n(d.totals?.turns), 0);
    const tokens = list.reduce((sum, d) => sum + tokenTotal(d.totals), 0);
    return { credits, turns, tokens, usd: credits * CONFIG.USD_PER_CREDIT };
  }

  function renderTable(list, stats) {
    return `
      <div class="table-container">
        <table class="compass-table">
          <thead>
            <tr><th>日期</th><th>Credits</th><th>Tokens</th><th>金额</th><th>轮数</th></tr>
          </thead>
          <tbody>
            ${[...list].reverse().map((row) => `
              <tr>
                <td>${row.date}</td>
                <td style="font-family:monospace">${n(row.totals?.credits).toFixed(3)}</td>
                <td style="font-family:monospace">${fmtNum(tokenTotal(row.totals))}</td>
                <td>$ ${(n(row.totals?.credits) * CONFIG.USD_PER_CREDIT).toFixed(2)}</td>
                <td>${n(row.totals?.turns)}</td>
              </tr>
            `).join("")}
          </tbody>
          <tfoot>
            <tr class="compass-footer-row">
              <td>合计</td>
              <td>${stats.credits.toFixed(3)}</td>
              <td style="font-family:monospace">${fmtNum(stats.tokens)}</td>
              <td style="color:#10a37f">$ ${stats.usd.toFixed(2)}</td>
              <td>${stats.turns}</td>
            </tr>
          </tfoot>
        </table>
      </div>
    `;
  }

  function showPanel(data) {
    const root = document.getElementById("codex-compass-root");
    const { secondary, dailyList, cycleStartDate } = data;

    const currentCycleList = [];
    const historyList = [];

    dailyList.forEach((item) => {
      if (new Date(item.date) >= new Date(cycleStartDate)) {
        currentCycleList.push(item);
      } else {
        historyList.push(item);
      }
    });

    const currentStats = getStats(currentCycleList);
    const historyStats = getStats(historyList);

    const ratio = n(secondary?.used_percent) / 100;
    const estCredits = ratio > 0 ? currentStats.credits / ratio : 0;

    let historyRangeTitle = "历史记录(本周期外)";
    if (historyList.length > 0) {
      const sortedHistory = [...historyList].sort(
        (a, b) => new Date(a.date) - new Date(b.date),
      );
      historyRangeTitle =
        `历史记录(本周期外 ${sortedHistory[0].date} 至 ${sortedHistory.at(-1).date})`;
    }

    root.innerHTML = `
      <div class="compass-header">
        <div class="compass-title">Codex 配额分析</div>
        <div class="compass-close" id="compass-close-btn">&times;</div>
      </div>

      <div class="compass-note">
        说明:analytics 可能延迟几小时;周期切换通常按美西时间计算。脚本会读取当前 chatgpt.com 页面内已有的 Bearer token,仅用于请求 chatgpt.com 同域 usage / analytics 接口;不读取 cookie 内容,不打印、不保存、不上传 token。
      </div>

      <div class="compass-grid">
        <div class="compass-card">
          <div class="card-label">已用比例</div>
          <div class="card-value">${n(secondary?.used_percent)}%</div>
        </div>
        <div class="compass-card">
          <div class="card-label">本周期已用</div>
          <div class="card-value">${currentStats.credits.toFixed(1)} Credits</div>
        </div>
        <div class="compass-card highlight">
          <div class="card-label">推算总额</div>
          <div class="card-value">${estCredits.toFixed(1)} Credits</div>
        </div>
        <div class="compass-card">
          <div class="card-label">周期价值估算</div>
          <div class="card-value">$ ${(estCredits * CONFIG.USD_PER_CREDIT).toFixed(2)}</div>
        </div>
      </div>

      <div style="font-weight:600; margin-bottom:8px; font-size:13px;">本周期明细(始于 ${cycleStartDate})</div>
      ${renderTable(currentCycleList, currentStats)}

      ${
        historyList.length > 0
          ? `<div style="font-weight:600; margin-bottom:8px; font-size:13px; color:#666;">${historyRangeTitle}</div>
             ${renderTable(historyList, historyStats)}`
          : ""
      }
    `;

    root.style.display = "flex";
    document.getElementById("compass-close-btn").onclick = () => {
      root.style.display = "none";
    };
  }

  async function run() {
    const btn = document.getElementById("codex-compass-btn");

    btn.innerText = "分析中...";

    try {
      const token = getPageBearerToken();

      const usage = await apiGet("/backend-api/wham/usage", token);

      const secondary =
        usage?.rate_limit?.secondary_window ||
        usage?.rate_limit?.primary_window;

      const endDate = new Date(Date.now() + 86400000)
        .toISOString()
        .split("T")[0];

      const startDate = new Date(Date.now() - 30 * 86400000)
        .toISOString()
        .split("T")[0];

      const cycleStartDate = secondary
        ? new Date((secondary.reset_at - secondary.limit_window_seconds) * 1000)
            .toISOString()
            .split("T")[0]
        : startDate;

      const dailyData = await apiGet(
        `/backend-api/wham/analytics/daily-workspace-usage-counts?start_date=${startDate}&end_date=${endDate}&group_by=day`,
        token,
      );

      showPanel({
        secondary,
        dailyList: dailyData.data || [],
        cycleStartDate,
      });
    } catch (e) {
      console.error("[Codex Compass]", e);
      alert("错误: " + e.message);
    } finally {
      btn.innerText = "运行用量分析";
    }
  }

  function removePanel() {
    document.getElementById("codex-compass-btn")?.remove();
    document.getElementById("codex-compass-root")?.remove();
  }

  function mount() {
    removePanel();

    const btn = document.createElement("button");
    btn.id = "codex-compass-btn";
    btn.innerText = "运行用量分析";
    btn.onclick = run;
    document.body.appendChild(btn);

    const root = document.createElement("div");
    root.id = "codex-compass-root";
    document.body.appendChild(root);
  }

  function boot() {
    if (location.pathname === TARGET_PATH) {
      if (!document.getElementById("codex-compass-btn")) {
        mount();
      }
    } else {
      removePanel();
    }
  }

  boot();
  setInterval(boot, 1000);
})();

继续阅读

基于全文检索与主题相似度