From c2f6e3f473b5c32b4fea069e0801756751f7a0d2 Mon Sep 17 00:00:00 2001
From: "K.T.S" <38993658@qq.com>
Date: Mon, 15 Jun 2026 17:46:24 +0800
Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=AF=8F?=
=?UTF-8?q?=E6=97=A5=20Token=20=E7=BB=9F=E8=AE=A1=E8=84=9A=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.json | 18 +
scripts/codex-daily-token-usage.js | 1793 ++++++++++++++++++++++++++++
2 files changed, 1811 insertions(+)
create mode 100644 scripts/codex-daily-token-usage.js
diff --git a/index.json b/index.json
index 75eefe8..19d0e79 100644
--- a/index.json
+++ b/index.json
@@ -68,6 +68,24 @@
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-token-usage.js",
"sha256": "03808d22f53da374837227636ebd9f1ba593fe5aba6219e90e636d7ed7c806eb"
},
+ {
+ "id": "codex-daily-token-usage",
+ "name": "Codex Daily Token Usage",
+ "description": "每日 Token 统计,自动复用已有采集,支持 5 日趋势和分享图",
+ "version": "1.3.0",
+ "author": "kts-kris",
+ "tags": [
+ "codex",
+ "tokens",
+ "usage",
+ "daily",
+ "share",
+ "ui"
+ ],
+ "homepage": "",
+ "script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-daily-token-usage.js",
+ "sha256": "f3b36d22ef38f6d3410fcf9865287a538eeb0d590b637a0f3243a95743bc5aea"
+ },
{
"id": "codex-list-pagebuster",
"name": "Codex List Pagebuster",
diff --git a/scripts/codex-daily-token-usage.js b/scripts/codex-daily-token-usage.js
new file mode 100644
index 0000000..ff5b230
--- /dev/null
+++ b/scripts/codex-daily-token-usage.js
@@ -0,0 +1,1793 @@
+// ==UserScript==
+// @name Codex Daily Token Usage
+// @namespace codex-plus-plus
+// @version 1.3.0
+// @description 每日 Token 统计,优先复用已有采集,必要时内置采集,支持日期切换、5 日趋势与分享图。
+// @match app://-/*
+// @run-at document-start
+// ==/UserScript==
+
+(() => {
+ "use strict";
+
+ const VERSION = "1.3.0";
+ const API_KEY = "__codexDailyTokenUsage";
+ const SOURCE_API_KEY = "__codexTokenUsage";
+ const STORAGE_KEY = "__codexDailyTokenUsageV1";
+ const ROOT_ID = "codex-daily-token-usage";
+ const PANEL_ID = "codex-daily-token-usage-panel";
+ const STYLE_ID = "codex-daily-token-usage-style";
+ const POLL_INTERVAL_MS = 1000;
+ const RETAIN_DAYS = 365;
+ const MAX_TURNS_PER_DAY = 2000;
+ const CAPTURE_DEDUPE_WINDOW_MS = 3000;
+ const MAX_CAPTURE_BODY_CHARS = 2_000_000;
+ const EXTERNAL_SOURCE_GRACE_MS = 4000;
+ const EXTERNAL_EMPTY_LIMIT = 4;
+ const TREND_DAYS = 5;
+
+ const previous = window[API_KEY];
+ if (previous && typeof previous.destroy === "function") {
+ try {
+ previous.destroy();
+ } catch {
+ // 旧实例清理失败不应阻止新实例加载。
+ }
+ }
+
+ let root = null;
+ let panel = null;
+ let style = null;
+ let observer = null;
+ let pollTimer = null;
+ let midnightTimer = null;
+ let closeTimer = null;
+ let shareFeedbackTimer = null;
+ let pinnedOpen = false;
+ let destroyed = false;
+ let sourceMode = "waiting";
+ let captureInstalled = false;
+ let lastCaptureAt = 0;
+ let captureSeq = 0;
+ let externalEmptyCount = 0;
+ let startedAt = Date.now();
+ let lastRenderedTotal = -1;
+ let lastDateKey = getDateKey(Date.now());
+ let selectedDateKey = lastDateKey;
+ let state = loadState();
+
+ function toCount(value) {
+ const number = Number(value);
+ return Number.isFinite(number) && number > 0 ? Math.round(number) : 0;
+ }
+
+ function firstCount(...values) {
+ for (const value of values) {
+ const number = toCount(value);
+ if (number > 0) return number;
+ }
+ return 0;
+ }
+
+ function getDateKey(timestamp) {
+ const date = new Date(timestamp);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ }
+
+ function parseDateKey(dateKey) {
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(dateKey || ""));
+ if (!match) return null;
+
+ const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
+ if (
+ date.getFullYear() !== Number(match[1]) ||
+ date.getMonth() !== Number(match[2]) - 1 ||
+ date.getDate() !== Number(match[3])
+ ) {
+ return null;
+ }
+ date.setHours(12, 0, 0, 0);
+ return date;
+ }
+
+ function shiftDateKey(dateKey, days) {
+ const date = parseDateKey(dateKey);
+ if (!date) return getDateKey(Date.now());
+ date.setDate(date.getDate() + Number(days || 0));
+ return getDateKey(date.getTime());
+ }
+
+ function getMinimumDateKey(now = Date.now()) {
+ const date = new Date(now);
+ date.setHours(12, 0, 0, 0);
+ date.setDate(date.getDate() - RETAIN_DAYS + 1);
+ return getDateKey(date.getTime());
+ }
+
+ function clampDateKey(dateKey, now = Date.now()) {
+ const parsed = parseDateKey(dateKey);
+ const todayKey = getDateKey(now);
+ const minimumKey = getMinimumDateKey(now);
+ if (!parsed) return todayKey;
+ return dateKey < minimumKey ? minimumKey : dateKey > todayKey ? todayKey : dateKey;
+ }
+
+ function getTurnTimestamp(turn) {
+ const encoded = Number.parseInt(String(turn?.turnId || "").split("-")[0], 10);
+ if (Number.isFinite(encoded) && encoded > 1_500_000_000_000) return encoded;
+
+ const createdAt = Date.parse(turn?.createdAt || "");
+ if (Number.isFinite(createdAt)) return createdAt;
+
+ return Date.now();
+ }
+
+ function normalizeUsage(rawUsage) {
+ const usage = rawUsage && typeof rawUsage === "object" ? rawUsage : {};
+ const input = firstCount(
+ usage.inputTotalTokens,
+ usage.inputTokens,
+ usage.input_tokens,
+ usage.prompt_tokens
+ );
+ const output = firstCount(
+ usage.outputTotalTokens,
+ usage.outputTokens,
+ usage.output_tokens,
+ usage.completion_tokens
+ );
+ const cached = firstCount(
+ usage.cachedReadTokens,
+ usage.cachedTokens,
+ usage.cacheReadTokens,
+ usage.cached_input_tokens,
+ usage.input_tokens_details?.cached_tokens
+ );
+ const reasoning = firstCount(
+ usage.reasoningTokens,
+ usage.reasoning_tokens,
+ usage.output_tokens_details?.reasoning_tokens
+ );
+ const total = firstCount(
+ usage.requestTotalTokens,
+ usage.totalTokens,
+ usage.total_tokens,
+ input + output
+ );
+
+ return { input, output, cached, reasoning, total };
+ }
+
+ function isUsageTurn(turn) {
+ if (!turn || typeof turn !== "object" || !turn.turnId) return false;
+ const usage = normalizeUsage(turn.usage);
+ return usage.total > 0 && (usage.input > 0 || usage.output > 0);
+ }
+
+ function createEmptyState() {
+ return { version: 1, days: {} };
+ }
+
+ function loadState() {
+ try {
+ const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null");
+ if (parsed?.version === 1 && parsed.days && typeof parsed.days === "object") {
+ return parsed;
+ }
+ } catch {
+ // 损坏或不可访问的本地数据按空状态处理。
+ }
+ return createEmptyState();
+ }
+
+ function saveState() {
+ try {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ function pruneState(now = Date.now()) {
+ const cutoff = new Date(now);
+ cutoff.setHours(0, 0, 0, 0);
+ cutoff.setDate(cutoff.getDate() - RETAIN_DAYS + 1);
+
+ for (const key of Object.keys(state.days)) {
+ const timestamp = Date.parse(`${key}T00:00:00`);
+ if (!Number.isFinite(timestamp) || timestamp < cutoff.getTime()) {
+ delete state.days[key];
+ }
+ }
+ }
+
+ function upsertTurn(turn) {
+ if (!isUsageTurn(turn)) return false;
+
+ const timestamp = getTurnTimestamp(turn);
+ const dateKey = getDateKey(timestamp);
+ const usage = normalizeUsage(turn.usage);
+ const day = state.days[dateKey] || { turns: {}, updatedAt: 0 };
+ const existing = day.turns[turn.turnId];
+ const candidate = {
+ input: usage.input,
+ output: usage.output,
+ cached: usage.cached,
+ reasoning: usage.reasoning,
+ total: usage.total,
+ calls: Math.max(1, toCount(turn.callCount)),
+ updatedAt: timestamp,
+ source: String(turn.source || "turn-aggregate"),
+ };
+
+ if (existing && existing.total > candidate.total) return false;
+
+ const next = existing
+ ? {
+ input: Math.max(existing.input || 0, candidate.input),
+ output: Math.max(existing.output || 0, candidate.output),
+ cached: Math.max(existing.cached || 0, candidate.cached),
+ reasoning: Math.max(existing.reasoning || 0, candidate.reasoning),
+ total: Math.max(existing.total || 0, candidate.total),
+ calls: Math.max(existing.calls || 0, candidate.calls),
+ updatedAt: Math.max(existing.updatedAt || 0, candidate.updatedAt),
+ source: candidate.source,
+ }
+ : candidate;
+
+ if (existing && JSON.stringify(existing) === JSON.stringify(next)) return false;
+
+ day.turns[turn.turnId] = next;
+ day.updatedAt = Math.max(day.updatedAt || 0, timestamp);
+
+ const turnIds = Object.keys(day.turns);
+ if (turnIds.length > MAX_TURNS_PER_DAY) {
+ turnIds
+ .sort((a, b) => (day.turns[a].updatedAt || 0) - (day.turns[b].updatedAt || 0))
+ .slice(0, turnIds.length - MAX_TURNS_PER_DAY)
+ .forEach((id) => delete day.turns[id]);
+ }
+
+ state.days[dateKey] = day;
+ return true;
+ }
+
+ function aggregateDay(dateKey = getDateKey(Date.now())) {
+ const turns = Object.values(state.days[dateKey]?.turns || {});
+ return turns.reduce(
+ (summary, turn) => {
+ summary.input += toCount(turn.input);
+ summary.output += toCount(turn.output);
+ summary.cached += toCount(turn.cached);
+ summary.reasoning += toCount(turn.reasoning);
+ summary.total += toCount(turn.total);
+ summary.calls += Math.max(1, toCount(turn.calls));
+ summary.turns += 1;
+ summary.updatedAt = Math.max(summary.updatedAt, toCount(turn.updatedAt));
+ return summary;
+ },
+ {
+ dateKey,
+ input: 0,
+ output: 0,
+ cached: 0,
+ reasoning: 0,
+ total: 0,
+ calls: 0,
+ turns: 0,
+ updatedAt: 0,
+ }
+ );
+ }
+
+ function formatTrendDateLabel(dateKey) {
+ const date = parseDateKey(dateKey);
+ if (!date) return dateKey;
+ return `${date.getMonth() + 1}/${date.getDate()}`;
+ }
+
+ function buildTrendData(dateKey = selectedDateKey, days = TREND_DAYS) {
+ const endKey = clampDateKey(dateKey);
+ const count = Math.max(1, toCount(days) || TREND_DAYS);
+ const items = [];
+ for (let offset = count - 1; offset >= 0; offset -= 1) {
+ const itemDateKey = shiftDateKey(endKey, -offset);
+ const summary = aggregateDay(itemDateKey);
+ items.push({
+ dateKey: itemDateKey,
+ label: formatTrendDateLabel(itemDateKey),
+ total: toCount(summary.total),
+ input: toCount(summary.input),
+ output: toCount(summary.output),
+ calls: toCount(summary.calls),
+ active: itemDateKey === endKey,
+ });
+ }
+ const maxTotal = Math.max(1, ...items.map((item) => item.total));
+ return { dateKey: endKey, days: count, maxTotal, items };
+ }
+
+ function trendPoints(trend, width = 286, height = 76, padding = 8) {
+ const items = trend?.items || [];
+ if (!items.length) return [];
+ const usableWidth = Math.max(1, width - padding * 2);
+ const usableHeight = Math.max(1, height - padding * 2);
+ const denominator = Math.max(1, items.length - 1);
+ return items.map((item, index) => ({
+ ...item,
+ x: padding + (usableWidth * index) / denominator,
+ y: padding + usableHeight - (usableHeight * item.total) / Math.max(1, trend.maxTotal),
+ }));
+ }
+
+ function trendPath(points, smooth = true) {
+ if (!points.length) return "";
+ if (points.length === 1 || !smooth) {
+ return points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(" ");
+ }
+
+ let path = `M ${points[0].x.toFixed(1)} ${points[0].y.toFixed(1)}`;
+ for (let index = 1; index < points.length; index += 1) {
+ const previous = points[index - 1];
+ const current = points[index];
+ const controlX = (previous.x + current.x) / 2;
+ path += ` C ${controlX.toFixed(1)} ${previous.y.toFixed(1)}, ${controlX.toFixed(1)} ${current.y.toFixed(1)}, ${current.x.toFixed(1)} ${current.y.toFixed(1)}`;
+ }
+ return path;
+ }
+
+ function formatCompact(value) {
+ const count = toCount(value);
+ if (count < 1000) return String(count);
+ if (count < 1_000_000) return `${stripTrailingZero(count / 1000)}K`;
+ if (count < 1_000_000_000) return `${stripTrailingZero(count / 1_000_000)}M`;
+ return `${stripTrailingZero(count / 1_000_000_000)}B`;
+ }
+
+ function stripTrailingZero(value) {
+ const digits = value >= 100 ? 0 : value >= 10 ? 1 : 2;
+ return String(Number(value.toFixed(digits)));
+ }
+
+ function formatExact(value) {
+ return new Intl.NumberFormat("zh-CN").format(toCount(value));
+ }
+
+ function formatTime(timestamp) {
+ if (!timestamp) return "暂无";
+ return new Intl.DateTimeFormat("zh-CN", {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ }).format(new Date(timestamp));
+ }
+
+ function formatDisplayDate(dateKey) {
+ const date = parseDateKey(dateKey);
+ if (!date) return dateKey;
+ return new Intl.DateTimeFormat("zh-CN", {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ weekday: "short",
+ }).format(date);
+ }
+
+ function buildShareModel(snapshot) {
+ const input = toCount(snapshot?.input);
+ const output = toCount(snapshot?.output);
+ const cached = toCount(snapshot?.cached);
+ const total = toCount(snapshot?.total);
+ return {
+ dateKey: snapshot?.dateKey || getDateKey(Date.now()),
+ dateLabel: formatDisplayDate(snapshot?.dateKey || getDateKey(Date.now())),
+ input,
+ output,
+ cached,
+ reasoning: toCount(snapshot?.reasoning),
+ total,
+ calls: toCount(snapshot?.calls),
+ turns: toCount(snapshot?.turns),
+ cacheRate: input > 0 ? Math.min(100, (cached / input) * 100) : 0,
+ outputRate: total > 0 ? Math.min(100, (output / total) * 100) : 0,
+ trend: buildTrendData(snapshot?.dateKey || getDateKey(Date.now())),
+ };
+ }
+
+ function roundedRectPath(context, x, y, width, height, radius) {
+ const r = Math.min(radius, width / 2, height / 2);
+ context.beginPath();
+ context.moveTo(x + r, y);
+ context.lineTo(x + width - r, y);
+ context.quadraticCurveTo(x + width, y, x + width, y + r);
+ context.lineTo(x + width, y + height - r);
+ context.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
+ context.lineTo(x + r, y + height);
+ context.quadraticCurveTo(x, y + height, x, y + height - r);
+ context.lineTo(x, y + r);
+ context.quadraticCurveTo(x, y, x + r, y);
+ context.closePath();
+ }
+
+ function fillRoundedRect(context, x, y, width, height, radius, fillStyle) {
+ roundedRectPath(context, x, y, width, height, radius);
+ context.fillStyle = fillStyle;
+ context.fill();
+ }
+
+ function drawMetricCard(context, { x, y, width, label, value, accent }) {
+ fillRoundedRect(context, x, y, width, 142, 24, "rgba(255, 255, 255, 0.075)");
+ context.strokeStyle = "rgba(255, 255, 255, 0.13)";
+ context.lineWidth = 1.5;
+ roundedRectPath(context, x, y, width, 142, 24);
+ context.stroke();
+
+ fillRoundedRect(context, x + 22, y + 22, 10, 10, 5, accent);
+ context.fillStyle = "rgba(226, 232, 255, 0.66)";
+ context.font = '500 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(label, x + 22, y + 63);
+
+ context.fillStyle = "#ffffff";
+ context.font = '700 34px -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif';
+ context.fillText(value, x + 22, y + 112);
+ }
+
+ function drawShareTrend(context, trend, x, y, width, height) {
+ fillRoundedRect(context, x, y, width, height, 28, "rgba(5, 8, 24, 0.4)");
+ context.strokeStyle = "rgba(255, 255, 255, 0.1)";
+ roundedRectPath(context, x, y, width, height, 28);
+ context.stroke();
+
+ context.fillStyle = "#ffffff";
+ context.font = '650 25px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText("近 5 日 Token 趋势", x + 30, y + 45);
+
+ context.textAlign = "right";
+ context.fillStyle = "rgba(226, 232, 255, 0.58)";
+ context.font = '500 20px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(`峰值 ${formatCompact(trend.maxTotal)}`, x + width - 30, y + 45);
+ context.textAlign = "left";
+
+ const chart = { x: x + 34, y: y + 62, width: width - 68, height: 88 };
+ context.strokeStyle = "rgba(255, 255, 255, 0.08)";
+ context.lineWidth = 1;
+ for (let index = 0; index < 3; index += 1) {
+ const gridY = chart.y + (chart.height * index) / 2;
+ context.beginPath();
+ context.moveTo(chart.x, gridY);
+ context.lineTo(chart.x + chart.width, gridY);
+ context.stroke();
+ }
+
+ const points = trendPoints(trend, chart.width, chart.height, 4).map((point) => ({
+ ...point,
+ x: point.x + chart.x,
+ y: point.y + chart.y,
+ }));
+ if (points.length > 0) {
+ const area = `${trendPath(points, true)} L ${points[points.length - 1].x.toFixed(1)} ${(chart.y + chart.height).toFixed(1)} L ${points[0].x.toFixed(1)} ${(chart.y + chart.height).toFixed(1)} Z`;
+ const areaGradient = context.createLinearGradient(0, chart.y, 0, chart.y + chart.height);
+ areaGradient.addColorStop(0, "rgba(76, 181, 255, 0.28)");
+ areaGradient.addColorStop(1, "rgba(123, 92, 255, 0.02)");
+ context.fillStyle = areaGradient;
+ const areaPath = new Path2D(area);
+ context.fill(areaPath);
+
+ const lineGradient = context.createLinearGradient(chart.x, 0, chart.x + chart.width, 0);
+ lineGradient.addColorStop(0, "#44B9FF");
+ lineGradient.addColorStop(1, "#9B7CFF");
+ context.strokeStyle = lineGradient;
+ context.lineWidth = 5;
+ context.lineCap = "round";
+ context.lineJoin = "round";
+ context.stroke(new Path2D(trendPath(points, true)));
+
+ for (const point of points) {
+ context.fillStyle = point.active ? "#FFFFFF" : "rgba(255, 255, 255, 0.78)";
+ context.beginPath();
+ context.arc(point.x, point.y, point.active ? 7 : 5, 0, Math.PI * 2);
+ context.fill();
+ context.fillStyle = point.active ? "#8A6CFF" : "#42B8FF";
+ context.beginPath();
+ context.arc(point.x, point.y, point.active ? 3 : 2.5, 0, Math.PI * 2);
+ context.fill();
+ }
+ }
+
+ context.fillStyle = "rgba(226, 232, 255, 0.52)";
+ context.font = '500 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.textAlign = "center";
+ const labelY = y + height - 26;
+ const denominator = Math.max(1, trend.items.length - 1);
+ trend.items.forEach((item, index) => {
+ const labelX = chart.x + (chart.width * index) / denominator;
+ context.fillStyle = item.active ? "#FFFFFF" : "rgba(226, 232, 255, 0.52)";
+ context.fillText(item.label, labelX, labelY);
+ });
+ context.textAlign = "left";
+ }
+
+ function createShareCanvas(dateKey = selectedDateKey) {
+ const model = buildShareModel(aggregateDay(clampDateKey(dateKey)));
+ const canvas = document.createElement("canvas");
+ canvas.width = 1200;
+ canvas.height = 900;
+ const context = canvas.getContext("2d");
+ if (!context) throw new Error("当前环境不支持 Canvas 2D");
+
+ const background = context.createLinearGradient(0, 0, 1200, 900);
+ background.addColorStop(0, "#070A18");
+ background.addColorStop(0.5, "#11132D");
+ background.addColorStop(1, "#21104A");
+ context.fillStyle = background;
+ context.fillRect(0, 0, 1200, 900);
+
+ const blueGlow = context.createRadialGradient(160, 120, 0, 160, 120, 430);
+ blueGlow.addColorStop(0, "rgba(35, 160, 255, 0.34)");
+ blueGlow.addColorStop(1, "rgba(35, 160, 255, 0)");
+ context.fillStyle = blueGlow;
+ context.fillRect(0, 0, 650, 650);
+
+ const purpleGlow = context.createRadialGradient(1080, 800, 0, 1080, 800, 520);
+ purpleGlow.addColorStop(0, "rgba(153, 72, 255, 0.34)");
+ purpleGlow.addColorStop(1, "rgba(153, 72, 255, 0)");
+ context.fillStyle = purpleGlow;
+ context.fillRect(500, 300, 700, 600);
+
+ context.fillStyle = "rgba(255, 255, 255, 0.035)";
+ for (let x = 40; x < 1200; x += 42) {
+ for (let y = 38; y < 900; y += 42) {
+ context.beginPath();
+ context.arc(x, y, 1.5, 0, Math.PI * 2);
+ context.fill();
+ }
+ }
+
+ fillRoundedRect(context, 70, 58, 194, 46, 23, "rgba(65, 166, 255, 0.16)");
+ context.strokeStyle = "rgba(89, 181, 255, 0.35)";
+ roundedRectPath(context, 70, 58, 194, 46, 23);
+ context.stroke();
+ context.fillStyle = "#72C3FF";
+ context.font = '700 19px -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif';
+ context.fillText("CODEX / TOKEN", 93, 89);
+
+ context.textAlign = "right";
+ context.fillStyle = "rgba(226, 232, 255, 0.72)";
+ context.font = '500 24px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(model.dateLabel, 1130, 87);
+ context.textAlign = "left";
+
+ context.fillStyle = "rgba(226, 232, 255, 0.66)";
+ context.font = '600 27px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText("当日累计 TOKEN", 72, 176);
+
+ const totalText = formatExact(model.total);
+ const totalFontSize = totalText.length > 12 ? 82 : totalText.length > 9 ? 98 : 116;
+ const totalGradient = context.createLinearGradient(70, 205, 800, 330);
+ totalGradient.addColorStop(0, "#FFFFFF");
+ totalGradient.addColorStop(0.55, "#A9DDFF");
+ totalGradient.addColorStop(1, "#C5A7FF");
+ context.fillStyle = totalGradient;
+ context.font = `750 ${totalFontSize}px -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif`;
+ context.fillText(totalText, 66, 304);
+
+ context.fillStyle = "rgba(226, 232, 255, 0.48)";
+ context.font = '500 21px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(`${formatExact(model.turns)} 个 turn · ${formatExact(model.calls)} 次请求`, 73, 350);
+
+ const cardWidth = 250;
+ const gap = 22;
+ const cardY = 410;
+ drawMetricCard(context, {
+ x: 70,
+ y: cardY,
+ width: cardWidth,
+ label: "输入 Token",
+ value: formatCompact(model.input),
+ accent: "#4FB6FF",
+ });
+ drawMetricCard(context, {
+ x: 70 + cardWidth + gap,
+ y: cardY,
+ width: cardWidth,
+ label: "输出 Token",
+ value: formatCompact(model.output),
+ accent: "#9D7CFF",
+ });
+ drawMetricCard(context, {
+ x: 70 + (cardWidth + gap) * 2,
+ y: cardY,
+ width: cardWidth,
+ label: "缓存输入",
+ value: formatCompact(model.cached),
+ accent: "#4EE4B1",
+ });
+ drawMetricCard(context, {
+ x: 70 + (cardWidth + gap) * 3,
+ y: cardY,
+ width: cardWidth,
+ label: "推理 Token",
+ value: formatCompact(model.reasoning),
+ accent: "#FFB35A",
+ });
+
+ drawShareTrend(context, model.trend, 70, 590, 1066, 188);
+
+ context.fillStyle = "rgba(226, 232, 255, 0.46)";
+ context.font = '500 19px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText("数据仅来自本机 Codex++,不包含会话内容", 72, 833);
+ context.textAlign = "right";
+ context.fillStyle = "rgba(114, 195, 255, 0.72)";
+ context.font = '650 19px -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif';
+ context.fillText("GENERATED LOCALLY", 1129, 833);
+ context.textAlign = "left";
+
+ return canvas;
+ }
+
+ function canvasToBlob(canvas) {
+ return new Promise((resolve, reject) => {
+ canvas.toBlob((blob) => {
+ if (blob) resolve(blob);
+ else reject(new Error("分享图片生成失败"));
+ }, "image/png");
+ });
+ }
+
+ async function createShareBlob(dateKey = selectedDateKey) {
+ return canvasToBlob(createShareCanvas(dateKey));
+ }
+
+ async function copyShareImage(dateKey = selectedDateKey) {
+ const clipboard = window.navigator?.clipboard;
+ const ClipboardItemClass = window.ClipboardItem;
+ if (!clipboard?.write || typeof ClipboardItemClass !== "function") {
+ throw new Error("当前环境不支持复制图片到剪贴板");
+ }
+
+ const blobPromise = createShareBlob(dateKey);
+ await clipboard.write([new ClipboardItemClass({ "image/png": blobPromise })]);
+ const blob = await blobPromise;
+ return { dateKey: clampDateKey(dateKey), size: blob.size, type: blob.type };
+ }
+
+ const recentCaptureKeys = new Map();
+
+ function cleanupRecentCaptureKeys(now = Date.now()) {
+ for (const [key, timestamp] of recentCaptureKeys) {
+ if (now - timestamp > CAPTURE_DEDUPE_WINDOW_MS * 4) {
+ recentCaptureKeys.delete(key);
+ }
+ }
+ }
+
+ function requestUrl(input) {
+ if (typeof input === "string") return input;
+ if (input?.url) return String(input.url);
+ return "";
+ }
+
+ function isLikelyUsageUrl(url) {
+ const text = String(url || "").toLowerCase();
+ return /codex|conversation|responses|completion|chat|backend-api|openai/.test(text);
+ }
+
+ function safeParseJson(text) {
+ if (typeof text !== "string") return null;
+ const trimmed = text.trim();
+ if (!trimmed) return null;
+ try {
+ return JSON.parse(trimmed);
+ } catch {
+ return null;
+ }
+ }
+
+ function parseTextPayloads(text) {
+ if (typeof text !== "string" || !text.trim() || text.length > MAX_CAPTURE_BODY_CHARS) {
+ return [];
+ }
+
+ const parsed = safeParseJson(text);
+ if (parsed) return [parsed];
+
+ const payloads = [];
+ for (const line of text.split(/\r?\n/)) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed === "data: [DONE]") continue;
+ const data = trimmed.startsWith("data:") ? trimmed.slice(5).trim() : trimmed;
+ const item = safeParseJson(data);
+ if (item) payloads.push(item);
+ }
+ return payloads;
+ }
+
+ function extractCandidateId(value) {
+ if (!value || typeof value !== "object") return "";
+ const candidates = [
+ value.response_id,
+ value.responseId,
+ value.request_id,
+ value.requestId,
+ value.event_id,
+ value.eventId,
+ value.message_id,
+ value.messageId,
+ value.id,
+ value.response?.id,
+ value.message?.id,
+ value.data?.id,
+ value.result?.id,
+ ];
+ const id = candidates.find((candidate) => typeof candidate === "string" && candidate.trim());
+ return id ? id.trim() : "";
+ }
+
+ function normalizeCaptureUsage(rawUsage) {
+ const usage = normalizeUsage(rawUsage);
+ if (!usage.total || (!usage.input && !usage.output)) return null;
+ return usage;
+ }
+
+ function findUsageCandidates(value, depth = 0, inheritedId = "") {
+ if (!value || depth > 8) return [];
+ if (typeof value === "string") {
+ return parseTextPayloads(value).flatMap((item) => findUsageCandidates(item, depth + 1, inheritedId));
+ }
+ if (Array.isArray(value)) {
+ return value.flatMap((item) => findUsageCandidates(item, depth + 1, inheritedId));
+ }
+ if (typeof value !== "object") return [];
+
+ const id = extractCandidateId(value) || inheritedId;
+ const candidates = [];
+ const directKeys = [
+ "usage",
+ "token_usage",
+ "tokenUsage",
+ "last_usage",
+ "lastUsage",
+ "last_token_usage",
+ "lastTokenUsage",
+ ];
+
+ for (const key of directKeys) {
+ const usage = normalizeCaptureUsage(value[key]);
+ if (usage) candidates.push({ usage, id });
+ }
+
+ const selfUsage = normalizeCaptureUsage(value);
+ if (selfUsage) candidates.push({ usage: selfUsage, id });
+
+ for (const key of [
+ "response",
+ "data",
+ "body",
+ "message",
+ "result",
+ "event",
+ "params",
+ "payload",
+ "delta",
+ "item",
+ "output",
+ "details",
+ ]) {
+ candidates.push(...findUsageCandidates(value[key], depth + 1, id));
+ }
+
+ return dedupeCandidates(candidates);
+ }
+
+ function usageSignature(usage) {
+ return [
+ toCount(usage.input),
+ toCount(usage.output),
+ toCount(usage.cached),
+ toCount(usage.reasoning),
+ toCount(usage.total),
+ ].join(":");
+ }
+
+ function dedupeCandidates(candidates) {
+ const seen = new Set();
+ return candidates.filter((candidate) => {
+ const key = `${candidate.id || ""}|${usageSignature(candidate.usage)}`;
+ if (seen.has(key)) return false;
+ seen.add(key);
+ return true;
+ });
+ }
+
+ function rememberCapturedUsage(candidate, source, url = "") {
+ const now = Date.now();
+ cleanupRecentCaptureKeys(now);
+ const signature = usageSignature(candidate.usage);
+ const dedupeKey = candidate.id
+ ? `id:${candidate.id}|${signature}`
+ : `near:${signature}|${Math.floor(now / CAPTURE_DEDUPE_WINDOW_MS)}`;
+ if (recentCaptureKeys.has(dedupeKey)) return false;
+ recentCaptureKeys.set(dedupeKey, now);
+
+ const turnId = candidate.id
+ ? `capture:${candidate.id}`
+ : `${now}-${++captureSeq}`;
+ const changed = upsertTurn({
+ turnId,
+ source: `capture:${source}`,
+ callCount: 1,
+ createdAt: new Date(now).toISOString(),
+ usage: {
+ inputTokens: candidate.usage.input,
+ outputTokens: candidate.usage.output,
+ cachedReadTokens: candidate.usage.cached,
+ reasoningTokens: candidate.usage.reasoning,
+ totalTokens: candidate.usage.total,
+ hasBreakdown: true,
+ },
+ url,
+ });
+
+ if (changed) {
+ lastCaptureAt = now;
+ sourceMode = "standalone";
+ }
+ return changed;
+ }
+
+ function processCapturePayload(payload, source, url = "") {
+ if (sourceMode === "external") return false;
+ const candidates = findUsageCandidates(payload);
+ if (!candidates.length) return false;
+ let changed = false;
+ for (const candidate of candidates) {
+ changed = rememberCapturedUsage(candidate, source, url) || changed;
+ }
+ if (changed) {
+ pruneState();
+ saveState();
+ render({ animate: true });
+ }
+ return changed;
+ }
+
+ function installFetchCapture() {
+ if (typeof window.fetch !== "function" || window.fetch.__codexDailyTokenUsageWrapped === VERSION) return;
+ const originalFetch = window.fetch;
+ async function wrappedFetch(input, init) {
+ const url = requestUrl(input);
+ const response = await originalFetch.call(this, input, init);
+ const contentType = String(response?.headers?.get?.("content-type") || "");
+ if (response?.clone && (isLikelyUsageUrl(url) || /json|event-stream|text/.test(contentType))) {
+ response
+ .clone()
+ .text()
+ .then((text) => processCapturePayload(text, "fetch", url))
+ .catch(() => {});
+ }
+ return response;
+ }
+ wrappedFetch.__codexDailyTokenUsageWrapped = VERSION;
+ wrappedFetch.__codexDailyTokenUsageOriginal = originalFetch;
+ window.fetch = wrappedFetch;
+ }
+
+ function installXhrCapture() {
+ const Xhr = window.XMLHttpRequest;
+ if (!Xhr?.prototype || Xhr.prototype.__codexDailyTokenUsageWrapped === VERSION) return;
+ const originalOpen = Xhr.prototype.open;
+ const originalSend = Xhr.prototype.send;
+ Xhr.prototype.open = function open(method, url, ...rest) {
+ this.__codexDailyTokenUsageUrl = url;
+ return originalOpen.call(this, method, url, ...rest);
+ };
+ Xhr.prototype.send = function send(...args) {
+ this.addEventListener?.("loadend", () => {
+ const url = this.__codexDailyTokenUsageUrl || "";
+ if (!isLikelyUsageUrl(url) && !String(this.getResponseHeader?.("content-type") || "").match(/json|event-stream|text/)) {
+ return;
+ }
+ try {
+ processCapturePayload(this.responseText || "", "xhr", url);
+ } catch {
+ // Ignore unreadable XHR bodies.
+ }
+ });
+ return originalSend.apply(this, args);
+ };
+ Xhr.prototype.__codexDailyTokenUsageOriginalOpen = originalOpen;
+ Xhr.prototype.__codexDailyTokenUsageOriginalSend = originalSend;
+ Xhr.prototype.__codexDailyTokenUsageWrapped = VERSION;
+ }
+
+ function installWebSocketCapture() {
+ if (typeof window.WebSocket !== "function" || window.WebSocket.__codexDailyTokenUsageWrapped === VERSION) return;
+ const NativeWebSocket = window.WebSocket;
+ function DailyTokenUsageWebSocket(...args) {
+ const socket = new NativeWebSocket(...args);
+ socket.addEventListener?.("message", (event) => {
+ try {
+ if (typeof event.data === "string") {
+ processCapturePayload(event.data, "websocket");
+ } else if (event.data instanceof Blob && event.data.size <= 512000) {
+ event.data.text().then((text) => processCapturePayload(text, "websocket")).catch(() => {});
+ }
+ } catch {
+ // Keep socket delivery untouched.
+ }
+ });
+ return socket;
+ }
+ try {
+ DailyTokenUsageWebSocket.prototype = NativeWebSocket.prototype;
+ Object.defineProperty(DailyTokenUsageWebSocket, "CONNECTING", { value: NativeWebSocket.CONNECTING });
+ Object.defineProperty(DailyTokenUsageWebSocket, "OPEN", { value: NativeWebSocket.OPEN });
+ Object.defineProperty(DailyTokenUsageWebSocket, "CLOSING", { value: NativeWebSocket.CLOSING });
+ Object.defineProperty(DailyTokenUsageWebSocket, "CLOSED", { value: NativeWebSocket.CLOSED });
+ } catch {
+ // Best-effort compatibility.
+ }
+ DailyTokenUsageWebSocket.__codexDailyTokenUsageWrapped = VERSION;
+ DailyTokenUsageWebSocket.__codexDailyTokenUsageOriginal = NativeWebSocket;
+ window.WebSocket = DailyTokenUsageWebSocket;
+ }
+
+ function installMessageCapture() {
+ if (window.__codexDailyTokenUsageMessageCapture === VERSION) return;
+ window.addEventListener?.(
+ "message",
+ (event) => {
+ try {
+ processCapturePayload(event.data, "post-message");
+ } catch {
+ // Ignore unrelated messages.
+ }
+ },
+ true
+ );
+ window.__codexDailyTokenUsageMessageCapture = VERSION;
+ }
+
+ function installStandaloneCapture() {
+ if (captureInstalled) return false;
+ installFetchCapture();
+ installXhrCapture();
+ installWebSocketCapture();
+ installMessageCapture();
+ captureInstalled = true;
+ sourceMode = "standalone";
+ return true;
+ }
+
+ function restoreStandaloneCapture() {
+ if (window.fetch?.__codexDailyTokenUsageWrapped === VERSION) {
+ window.fetch = window.fetch.__codexDailyTokenUsageOriginal;
+ }
+ const Xhr = window.XMLHttpRequest;
+ if (Xhr?.prototype?.__codexDailyTokenUsageWrapped === VERSION) {
+ Xhr.prototype.open = Xhr.prototype.__codexDailyTokenUsageOriginalOpen;
+ Xhr.prototype.send = Xhr.prototype.__codexDailyTokenUsageOriginalSend;
+ delete Xhr.prototype.__codexDailyTokenUsageWrapped;
+ }
+ if (window.WebSocket?.__codexDailyTokenUsageWrapped === VERSION) {
+ window.WebSocket = window.WebSocket.__codexDailyTokenUsageOriginal;
+ }
+ captureInstalled = false;
+ }
+
+ function readExternalTurns() {
+ const source = window[SOURCE_API_KEY];
+ if (!source || typeof source.export !== "function") {
+ return [];
+ }
+
+ try {
+ const exported = source.export();
+ return Array.isArray(exported?.turns) ? exported.turns : [];
+ } catch {
+ return [];
+ }
+ }
+
+ function externalSourceAvailable() {
+ return typeof window[SOURCE_API_KEY]?.export === "function";
+ }
+
+ function shouldInstallStandaloneCapture(externalTurns) {
+ if (captureInstalled) return false;
+ if (!externalSourceAvailable()) {
+ return Date.now() - startedAt >= EXTERNAL_SOURCE_GRACE_MS;
+ }
+ if (externalTurns.length > 0) return false;
+ externalEmptyCount += 1;
+ return externalEmptyCount >= EXTERNAL_EMPTY_LIMIT;
+ }
+
+ function syncFromSource() {
+ let changed = false;
+ const externalTurns = readExternalTurns();
+
+ if (externalTurns.length > 0) {
+ sourceMode = "external";
+ externalEmptyCount = 0;
+ if (captureInstalled) restoreStandaloneCapture();
+ } else if (captureInstalled) {
+ sourceMode = "standalone";
+ } else {
+ sourceMode = "waiting";
+ }
+
+ for (const turn of externalTurns) {
+ changed = upsertTurn(turn) || changed;
+ }
+
+ if (shouldInstallStandaloneCapture(externalTurns)) {
+ installStandaloneCapture();
+ }
+
+ if (changed) {
+ pruneState();
+ saveState();
+ }
+ return changed;
+ }
+
+ function installStyle() {
+ if (document.getElementById(STYLE_ID)) {
+ style = document.getElementById(STYLE_ID);
+ return;
+ }
+
+ style = document.createElement("style");
+ style.id = STYLE_ID;
+ style.textContent = `
+ #${ROOT_ID} {
+ position: relative;
+ display: flex;
+ flex: 0 0 auto;
+ align-items: center;
+ pointer-events: auto;
+ -webkit-app-region: no-drag;
+ z-index: 2147483600;
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ }
+ #${ROOT_ID}.codex-daily-floating {
+ position: fixed;
+ top: 10px;
+ right: 80px;
+ }
+ #${ROOT_ID} .codex-daily-trigger {
+ box-sizing: border-box;
+ height: 31px;
+ min-width: 94px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 0 10px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.22));
+ border-radius: 9px;
+ color: var(--color-token-foreground, #202020);
+ background: var(--color-token-background-secondary, rgba(127, 127, 127, 0.08));
+ box-shadow: none;
+ cursor: default;
+ font: inherit;
+ font-size: 12px;
+ line-height: 1;
+ white-space: nowrap;
+ user-select: none;
+ outline: none;
+ transition: background 120ms ease, border-color 120ms ease, transform 120ms ease;
+ -webkit-app-region: no-drag;
+ }
+ #${ROOT_ID} .codex-daily-trigger:hover,
+ #${ROOT_ID} .codex-daily-trigger:focus-visible,
+ #${ROOT_ID}.is-open .codex-daily-trigger {
+ background: var(--color-token-background-tertiary, rgba(127, 127, 127, 0.15));
+ border-color: var(--color-token-border-strong, rgba(127, 127, 127, 0.36));
+ }
+ #${ROOT_ID}.is-updated .codex-daily-trigger {
+ animation: codex-daily-token-pulse 420ms ease;
+ }
+ #${ROOT_ID} .codex-daily-sigma {
+ width: 16px;
+ height: 16px;
+ display: inline-grid;
+ place-items: center;
+ border-radius: 5px;
+ background: rgba(74, 144, 226, 0.14);
+ color: #4a90e2;
+ font-size: 12px;
+ font-weight: 700;
+ }
+ #${ROOT_ID} .codex-daily-total {
+ font-variant-numeric: tabular-nums;
+ font-weight: 600;
+ letter-spacing: 0.01em;
+ }
+ #${PANEL_ID} {
+ position: fixed;
+ width: min(350px, calc(100vw - 24px));
+ box-sizing: border-box;
+ padding: 14px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.24));
+ border-radius: 12px;
+ color: var(--color-token-foreground, #202020);
+ background: var(--color-token-background, #ffffff);
+ box-shadow: 0 14px 40px rgba(0, 0, 0, 0.18);
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-4px);
+ transition: opacity 120ms ease, visibility 120ms ease, transform 120ms ease;
+ pointer-events: none;
+ cursor: default;
+ z-index: 2147483647;
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ -webkit-app-region: no-drag;
+ }
+ #${PANEL_ID}.is-visible {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ pointer-events: auto;
+ }
+ #${PANEL_ID} .codex-daily-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+ }
+ #${PANEL_ID} .codex-daily-title {
+ font-size: 13px;
+ font-weight: 650;
+ }
+ #${PANEL_ID} .codex-daily-date-nav {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ padding: 2px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.2));
+ border-radius: 8px;
+ background: var(--color-token-background-secondary, rgba(127, 127, 127, 0.07));
+ }
+ #${PANEL_ID} .codex-daily-date-button {
+ width: 23px;
+ height: 23px;
+ display: inline-grid;
+ place-items: center;
+ padding: 0;
+ border: 0;
+ border-radius: 6px;
+ color: var(--color-token-foreground-secondary, #737373);
+ background: transparent;
+ cursor: pointer;
+ font: inherit;
+ font-size: 16px;
+ line-height: 1;
+ }
+ #${PANEL_ID} .codex-daily-date-button:hover:not(:disabled) {
+ color: var(--color-token-foreground, #202020);
+ background: var(--color-token-background-tertiary, rgba(127, 127, 127, 0.14));
+ }
+ #${PANEL_ID} .codex-daily-date-button:disabled {
+ cursor: default;
+ opacity: 0.28;
+ }
+ #${PANEL_ID} .codex-daily-date-input {
+ width: 105px;
+ height: 23px;
+ box-sizing: border-box;
+ padding: 0 2px;
+ border: 0;
+ outline: 0;
+ color: var(--color-token-foreground-secondary, #737373);
+ background: transparent;
+ cursor: pointer;
+ font: inherit;
+ font-size: 11px;
+ line-height: 23px;
+ color-scheme: light dark;
+ }
+ #${PANEL_ID} .codex-daily-hero {
+ margin-bottom: 12px;
+ font-size: 26px;
+ line-height: 1.1;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+ }
+ #${PANEL_ID} .codex-daily-grid {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ gap: 8px 16px;
+ padding: 11px 0;
+ border-top: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.18));
+ border-bottom: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.18));
+ font-size: 12px;
+ }
+ #${PANEL_ID} .codex-daily-label {
+ color: var(--color-token-foreground-secondary, #737373);
+ }
+ #${PANEL_ID} .codex-daily-value {
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+ font-weight: 550;
+ }
+ #${PANEL_ID} .codex-daily-trend {
+ margin-top: 11px;
+ padding: 10px 11px 9px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.18));
+ border-radius: 10px;
+ background: var(--color-token-background-secondary, rgba(127, 127, 127, 0.06));
+ }
+ #${PANEL_ID} .codex-daily-trend-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 6px;
+ font-size: 11px;
+ }
+ #${PANEL_ID} .codex-daily-trend-title {
+ color: var(--color-token-foreground, #202020);
+ font-weight: 650;
+ }
+ #${PANEL_ID} .codex-daily-trend-peak {
+ color: var(--color-token-foreground-secondary, #737373);
+ font-variant-numeric: tabular-nums;
+ }
+ #${PANEL_ID} .codex-daily-trend-svg {
+ display: block;
+ width: 100%;
+ height: 82px;
+ overflow: visible;
+ }
+ #${PANEL_ID} .codex-daily-trend-labels {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 4px;
+ margin-top: 4px;
+ color: var(--color-token-foreground-secondary, #737373);
+ font-size: 10px;
+ font-variant-numeric: tabular-nums;
+ text-align: center;
+ }
+ #${PANEL_ID} .codex-daily-trend-labels span.is-active {
+ color: var(--color-token-foreground, #202020);
+ font-weight: 650;
+ }
+ #${PANEL_ID} .codex-daily-foot {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-top: 11px;
+ color: var(--color-token-foreground-secondary, #737373);
+ font-size: 11px;
+ line-height: 1.45;
+ }
+ #${PANEL_ID} .codex-daily-status-wrap {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ }
+ #${PANEL_ID} .codex-daily-status-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ #${PANEL_ID} .codex-daily-status {
+ width: 7px;
+ height: 7px;
+ flex: 0 0 auto;
+ border-radius: 50%;
+ background: #c58a22;
+ }
+ #${PANEL_ID}.is-connected .codex-daily-status {
+ background: #2e9d58;
+ }
+ #${PANEL_ID} .codex-daily-share {
+ height: 29px;
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: 5px;
+ padding: 0 9px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.22));
+ border-radius: 8px;
+ color: var(--color-token-foreground, #202020);
+ background: var(--color-token-background-secondary, rgba(127, 127, 127, 0.08));
+ cursor: pointer;
+ font: inherit;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1;
+ white-space: nowrap;
+ }
+ #${PANEL_ID} .codex-daily-share:hover:not(:disabled) {
+ border-color: rgba(74, 144, 226, 0.4);
+ background: rgba(74, 144, 226, 0.12);
+ }
+ #${PANEL_ID} .codex-daily-share:disabled {
+ cursor: wait;
+ opacity: 0.65;
+ }
+ #${PANEL_ID} .codex-daily-share[data-state="success"] {
+ color: #24834a;
+ border-color: rgba(46, 157, 88, 0.35);
+ background: rgba(46, 157, 88, 0.1);
+ }
+ #${PANEL_ID} .codex-daily-share[data-state="error"] {
+ color: #bf3f48;
+ border-color: rgba(191, 63, 72, 0.35);
+ background: rgba(191, 63, 72, 0.1);
+ }
+ #${PANEL_ID} .codex-daily-share svg {
+ width: 14px;
+ height: 14px;
+ }
+ @keyframes codex-daily-token-pulse {
+ 0%, 100% { transform: scale(1); }
+ 45% { transform: scale(1.035); }
+ }
+ @media (prefers-color-scheme: dark) {
+ #${PANEL_ID} {
+ background: var(--color-token-background, #202020);
+ color: var(--color-token-foreground, #f2f2f2);
+ }
+ }
+ `;
+ (document.head || document.documentElement).appendChild(style);
+ }
+
+ function createRoot() {
+ root = document.createElement("div");
+ root.id = ROOT_ID;
+ root.innerHTML = `
+
+ `;
+
+ panel = document.createElement("section");
+ panel.id = PANEL_ID;
+ panel.setAttribute("aria-label", "Token 用量明细");
+ panel.innerHTML = `
+
+ Token 用量
+
+
+
+
+
+
+ 0
+
+ 输入 Token0
+ 输出 Token0
+ 缓存输入0
+ 推理 Token0
+ 请求次数0
+ 最近更新暂无
+
+
+
+ 近 5 日趋势
+ 峰值 0
+
+
+
+
+
+ `;
+ document.body.appendChild(panel);
+
+ const trigger = root.querySelector(".codex-daily-trigger");
+ trigger.addEventListener("click", () => {
+ pinnedOpen = !pinnedOpen;
+ if (pinnedOpen) {
+ showPanel();
+ } else if (!root.matches(":hover") && !panel.matches(":hover")) {
+ hidePanel();
+ }
+ });
+ trigger.addEventListener("keydown", (event) => {
+ if (event.key === "Escape") {
+ pinnedOpen = false;
+ hidePanel();
+ trigger.blur();
+ }
+ });
+ trigger.addEventListener("focus", showPanel);
+ trigger.addEventListener("blur", schedulePanelClose);
+ root.addEventListener("mouseenter", showPanel);
+ root.addEventListener("mouseleave", schedulePanelClose);
+ panel.addEventListener("mouseenter", cancelPanelClose);
+ panel.addEventListener("mouseleave", schedulePanelClose);
+
+ panel.querySelector('[data-action="previous-day"]').addEventListener("click", () => {
+ selectDate(shiftDateKey(selectedDateKey, -1));
+ });
+ panel.querySelector('[data-action="next-day"]').addEventListener("click", () => {
+ selectDate(shiftDateKey(selectedDateKey, 1));
+ });
+ panel.querySelector(".codex-daily-date-input").addEventListener("change", (event) => {
+ selectDate(event.currentTarget.value);
+ });
+ panel.querySelector(".codex-daily-share").addEventListener("click", handleShareClick);
+ }
+
+ function selectDate(dateKey) {
+ selectedDateKey = clampDateKey(dateKey);
+ render();
+ return aggregateDay(selectedDateKey);
+ }
+
+ async function handleShareClick(event) {
+ const button = event.currentTarget;
+ const label = button.querySelector("span");
+ pinnedOpen = true;
+ showPanel();
+ button.disabled = true;
+ button.dataset.state = "loading";
+ label.textContent = "生成中";
+
+ if (shareFeedbackTimer) window.clearTimeout(shareFeedbackTimer);
+ try {
+ await copyShareImage(selectedDateKey);
+ button.dataset.state = "success";
+ label.textContent = "已复制";
+ } catch (error) {
+ button.dataset.state = "error";
+ label.textContent = "复制失败";
+ console.warn("[codex-daily-token-usage] share failed", error);
+ } finally {
+ button.disabled = false;
+ shareFeedbackTimer = window.setTimeout(() => {
+ if (!button.isConnected) return;
+ button.dataset.state = "idle";
+ label.textContent = "分享";
+ }, 2200);
+ }
+ }
+
+ function positionPanel() {
+ if (!root || !panel) return;
+ const rect = root.getBoundingClientRect();
+ panel.style.top = `${Math.round(rect.bottom + 8)}px`;
+ panel.style.right = `${Math.max(12, Math.round(innerWidth - rect.right))}px`;
+ }
+
+ function cancelPanelClose() {
+ if (closeTimer) {
+ window.clearTimeout(closeTimer);
+ closeTimer = null;
+ }
+ }
+
+ function showPanel() {
+ if (!root || !panel) return;
+ cancelPanelClose();
+ positionPanel();
+ root.classList.add("is-open");
+ root.querySelector(".codex-daily-trigger")?.setAttribute("aria-expanded", "true");
+ panel.classList.add("is-visible");
+ }
+
+ function hidePanel() {
+ if (!root || !panel) return;
+ cancelPanelClose();
+ root.classList.remove("is-open");
+ root.querySelector(".codex-daily-trigger")?.setAttribute("aria-expanded", "false");
+ panel.classList.remove("is-visible");
+ }
+
+ function schedulePanelClose() {
+ cancelPanelClose();
+ closeTimer = window.setTimeout(() => {
+ if (!pinnedOpen && !root?.matches(":hover") && !panel?.matches(":hover")) {
+ hidePanel();
+ }
+ }, 140);
+ }
+
+ function handleDocumentPointerDown(event) {
+ if (!pinnedOpen || root?.contains(event.target) || panel?.contains(event.target)) return;
+ pinnedOpen = false;
+ hidePanel();
+ }
+
+ function findToolbar() {
+ const plusMenu = document.getElementById("codex-plus-menu");
+ if (plusMenu?.parentElement) return plusMenu.parentElement;
+
+ const candidates = Array.from(document.querySelectorAll("button")).filter((button) => {
+ const rect = button.getBoundingClientRect();
+ return rect.width > 0 && rect.height > 0 && rect.top >= 0 && rect.top < 60 && rect.right > innerWidth - 360;
+ });
+
+ for (const button of candidates) {
+ let current = button.parentElement;
+ for (let depth = 0; current && depth < 4; depth += 1, current = current.parentElement) {
+ const style = getComputedStyle(current);
+ const rect = current.getBoundingClientRect();
+ if (
+ style.display === "flex" &&
+ rect.height > 20 &&
+ rect.height < 60 &&
+ rect.right > innerWidth - 24 &&
+ current.children.length > 1
+ ) {
+ return current;
+ }
+ }
+ }
+ return null;
+ }
+
+ function mountRoot() {
+ if (!root || destroyed || !document.body) return;
+ const toolbar = findToolbar();
+
+ if (toolbar) {
+ root.classList.remove("codex-daily-floating");
+ if (root.parentElement !== toolbar) toolbar.insertBefore(root, toolbar.firstChild);
+ if (panel?.classList.contains("is-visible")) positionPanel();
+ return;
+ }
+
+ root.classList.add("codex-daily-floating");
+ if (root.parentElement !== document.body) document.body.appendChild(root);
+ if (panel?.classList.contains("is-visible")) positionPanel();
+ }
+
+ function sourceStatusText(snapshot) {
+ if (sourceMode === "external") {
+ return `${snapshot.turns} 个 turn · 复用 Codex Token Usage`;
+ }
+ if (sourceMode === "standalone") {
+ return `${snapshot.turns} 个 turn · 本机累计 · 独立采集`;
+ }
+ return externalSourceAvailable() ? "等待 Codex Token Usage 数据" : "等待数据源,必要时自动采集";
+ }
+
+ function renderPanelTrend(trend) {
+ if (!panel) return;
+ const svg = panel.querySelector(".codex-daily-trend-svg");
+ const labels = panel.querySelector(".codex-daily-trend-labels");
+ const peak = panel.querySelector('[data-field="trendPeak"]');
+ if (peak) peak.textContent = `峰值 ${formatCompact(trend?.maxTotal || 0)}`;
+
+ const points = trendPoints(trend, 300, 82, 8);
+ const line = trendPath(points, true);
+ const baseline = 74;
+ const area =
+ points.length > 0
+ ? `${line} L ${points[points.length - 1].x.toFixed(1)} ${baseline.toFixed(1)} L ${points[0].x.toFixed(1)} ${baseline.toFixed(1)} Z`
+ : "";
+
+ if (svg) {
+ svg.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+ ${area ? `` : ""}
+ ${line ? `` : ""}
+ ${points
+ .map(
+ (point) => `
+
+ `
+ )
+ .join("")}
+ `;
+ }
+
+ if (labels) {
+ labels.replaceChildren(
+ ...(trend?.items || []).map((item) => {
+ const label = document.createElement("span");
+ label.textContent = item.label;
+ label.title = `${item.dateKey} · ${formatExact(item.total)} Token`;
+ if (item.active) label.classList.add("is-active");
+ return label;
+ })
+ );
+ }
+ }
+
+ function render({ animate = false } = {}) {
+ if (!root) return;
+ const todayKey = getDateKey(Date.now());
+ const todaySnapshot = aggregateDay(todayKey);
+ const snapshot = aggregateDay(selectedDateKey);
+ const totalChanged = lastRenderedTotal >= 0 && todaySnapshot.total !== lastRenderedTotal;
+ lastRenderedTotal = todaySnapshot.total;
+
+ const connected = sourceMode === "external" || sourceMode === "standalone";
+ root.classList.toggle("is-connected", connected);
+ panel?.classList.toggle("is-connected", connected);
+ root.querySelector(".codex-daily-total").textContent = formatCompact(todaySnapshot.total);
+ panel.querySelector(".codex-daily-title").textContent =
+ selectedDateKey === todayKey ? "今日 Token 用量" : "Token 用量";
+ panel.querySelector(".codex-daily-hero").textContent = formatExact(snapshot.total);
+ panel.querySelector('[data-field="input"]').textContent = formatExact(snapshot.input);
+ panel.querySelector('[data-field="output"]').textContent = formatExact(snapshot.output);
+ panel.querySelector('[data-field="cached"]').textContent = formatExact(snapshot.cached);
+ panel.querySelector('[data-field="reasoning"]').textContent = formatExact(snapshot.reasoning);
+ panel.querySelector('[data-field="calls"]').textContent = formatExact(snapshot.calls);
+ panel.querySelector('[data-field="updatedAt"]').textContent = formatTime(snapshot.updatedAt);
+ const dateInput = panel.querySelector(".codex-daily-date-input");
+ dateInput.min = getMinimumDateKey();
+ dateInput.max = todayKey;
+ dateInput.value = selectedDateKey;
+ panel.querySelector('[data-action="previous-day"]').disabled =
+ selectedDateKey <= dateInput.min;
+ panel.querySelector('[data-action="next-day"]').disabled =
+ selectedDateKey >= todayKey;
+ panel.querySelector(".codex-daily-status-text").textContent = sourceStatusText(snapshot);
+ renderPanelTrend(buildTrendData(selectedDateKey));
+
+ if (animate && totalChanged) {
+ root.classList.remove("is-updated");
+ void root.offsetWidth;
+ root.classList.add("is-updated");
+ window.setTimeout(() => root?.classList.remove("is-updated"), 450);
+ }
+ }
+
+ function scheduleMidnightRefresh() {
+ if (midnightTimer) window.clearTimeout(midnightTimer);
+ const next = new Date();
+ next.setHours(24, 0, 1, 0);
+ midnightTimer = window.setTimeout(() => {
+ const wasViewingToday = selectedDateKey === lastDateKey;
+ lastDateKey = getDateKey(Date.now());
+ if (wasViewingToday) selectedDateKey = lastDateKey;
+ pruneState();
+ saveState();
+ render();
+ scheduleMidnightRefresh();
+ }, Math.max(1000, next.getTime() - Date.now()));
+ }
+
+ function refresh() {
+ if (destroyed) return aggregateDay();
+
+ const currentDateKey = getDateKey(Date.now());
+ if (currentDateKey !== lastDateKey) {
+ const wasViewingToday = selectedDateKey === lastDateKey;
+ lastDateKey = currentDateKey;
+ if (wasViewingToday) selectedDateKey = currentDateKey;
+ pruneState();
+ saveState();
+ }
+
+ const changed = syncFromSource();
+ mountRoot();
+ render({ animate: changed });
+ return aggregateDay();
+ }
+
+ function resetToday() {
+ delete state.days[getDateKey(Date.now())];
+ saveState();
+ render();
+ return aggregateDay();
+ }
+
+ function destroy(options = {}) {
+ destroyed = true;
+ if (pollTimer) window.clearInterval(pollTimer);
+ if (midnightTimer) window.clearTimeout(midnightTimer);
+ if (closeTimer) window.clearTimeout(closeTimer);
+ if (shareFeedbackTimer) window.clearTimeout(shareFeedbackTimer);
+ observer?.disconnect();
+ restoreStandaloneCapture();
+ document.removeEventListener("pointerdown", handleDocumentPointerDown, true);
+ window.removeEventListener("resize", positionPanel);
+ root?.remove();
+ panel?.remove();
+ style?.remove();
+ if (options.clearData === true) {
+ try {
+ localStorage.removeItem(STORAGE_KEY);
+ } catch {
+ // localStorage 不可访问时忽略。
+ }
+ }
+ if (window[API_KEY] === api) delete window[API_KEY];
+ }
+
+ const api = {
+ version: VERSION,
+ refresh,
+ getSnapshot: (dateKey = getDateKey(Date.now())) => aggregateDay(clampDateKey(dateKey)),
+ getSelectedDate: () => selectedDateKey,
+ selectDate,
+ createShareBlob,
+ copyShareImage,
+ resetToday,
+ destroy,
+ __test: {
+ normalizeUsage,
+ getDateKey,
+ parseDateKey,
+ shiftDateKey,
+ clampDateKey,
+ getMinimumDateKey,
+ getTurnTimestamp,
+ isUsageTurn,
+ upsertTurn,
+ aggregateDay,
+ formatCompact,
+ buildTrendData,
+ trendPoints,
+ trendPath,
+ buildShareModel,
+ findUsageCandidates,
+ processCapturePayload,
+ syncFromSource,
+ installStandaloneCapture,
+ externalSourceAvailable,
+ getSourceMode: () => sourceMode,
+ isCaptureInstalled: () => captureInstalled,
+ setStartedAt(value) {
+ startedAt = value;
+ },
+ replaceState(nextState) {
+ state = nextState;
+ },
+ },
+ };
+
+ window[API_KEY] = api;
+
+ function start() {
+ if (destroyed) return;
+ installStyle();
+ createRoot();
+ mountRoot();
+ refresh();
+
+ observer = new MutationObserver(() => mountRoot());
+ observer.observe(document.documentElement, { childList: true, subtree: true });
+ document.addEventListener("pointerdown", handleDocumentPointerDown, true);
+ window.addEventListener("resize", positionPanel);
+ pollTimer = window.setInterval(refresh, POLL_INTERVAL_MS);
+ scheduleMidnightRefresh();
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", start, { once: true });
+ } else {
+ start();
+ }
+})();
From e3f6c6d66ae1453546c7b96e9cf2457cb5ad499a Mon Sep 17 00:00:00 2001
From: "K.T.S" <38993658@qq.com>
Date: Tue, 16 Jun 2026 12:43:11 +0800
Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20Model=20?=
=?UTF-8?q?=E4=BB=B7=E6=A0=BC=E6=88=90=E6=9C=AC=E4=BC=B0=E7=AE=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.json | 8 +-
scripts/codex-daily-token-usage.js | 779 ++++++++++++++++++++++++++++-
2 files changed, 768 insertions(+), 19 deletions(-)
diff --git a/index.json b/index.json
index 19d0e79..9660868 100644
--- a/index.json
+++ b/index.json
@@ -1,6 +1,6 @@
{
"version": 1,
- "updated_at": "2026-06-08T06:45:59.000Z",
+ "updated_at": "2026-06-16T04:41:14.000Z",
"scripts": [
{
"id": "codex-context-ring-restore",
@@ -71,8 +71,8 @@
{
"id": "codex-daily-token-usage",
"name": "Codex Daily Token Usage",
- "description": "每日 Token 统计,自动复用已有采集,支持 5 日趋势和分享图",
- "version": "1.3.0",
+ "description": "每日 Token 统计,自动复用已有采集,支持 Model 价格、成本估算、5 日趋势和分享图",
+ "version": "1.4.0",
"author": "kts-kris",
"tags": [
"codex",
@@ -84,7 +84,7 @@
],
"homepage": "",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-daily-token-usage.js",
- "sha256": "f3b36d22ef38f6d3410fcf9865287a538eeb0d590b637a0f3243a95743bc5aea"
+ "sha256": "e87d7eb7be5374275fbf7c0f0c3a86a43ac51f02be27febb0a496f7fa8cbaf55"
},
{
"id": "codex-list-pagebuster",
diff --git a/scripts/codex-daily-token-usage.js b/scripts/codex-daily-token-usage.js
index ff5b230..1842897 100644
--- a/scripts/codex-daily-token-usage.js
+++ b/scripts/codex-daily-token-usage.js
@@ -1,8 +1,8 @@
// ==UserScript==
// @name Codex Daily Token Usage
// @namespace codex-plus-plus
-// @version 1.3.0
-// @description 每日 Token 统计,优先复用已有采集,必要时内置采集,支持日期切换、5 日趋势与分享图。
+// @version 1.4.0
+// @description 每日 Token 统计,优先复用已有采集,必要时内置采集,支持 Model 价格、成本估算、日期切换、5 日趋势与分享图。
// @match app://-/*
// @run-at document-start
// ==/UserScript==
@@ -10,10 +10,11 @@
(() => {
"use strict";
- const VERSION = "1.3.0";
+ const VERSION = "1.4.0";
const API_KEY = "__codexDailyTokenUsage";
const SOURCE_API_KEY = "__codexTokenUsage";
const STORAGE_KEY = "__codexDailyTokenUsageV1";
+ const PRICE_STORAGE_KEY = "__codexDailyTokenUsageModelPricesV1";
const ROOT_ID = "codex-daily-token-usage";
const PANEL_ID = "codex-daily-token-usage-panel";
const STYLE_ID = "codex-daily-token-usage-style";
@@ -25,6 +26,9 @@
const EXTERNAL_SOURCE_GRACE_MS = 4000;
const EXTERNAL_EMPTY_LIMIT = 4;
const TREND_DAYS = 5;
+ const MODEL_BIND_WINDOW_MS = 180000;
+ const UNKNOWN_MODEL = "Unknown";
+ const PRICE_FIELDS = ["input", "cachedInput", "output", "reasoning"];
const previous = window[API_KEY];
if (previous && typeof previous.destroy === "function") {
@@ -47,6 +51,7 @@
let destroyed = false;
let sourceMode = "waiting";
let captureInstalled = false;
+ let modelCaptureInstalled = false;
let lastCaptureAt = 0;
let captureSeq = 0;
let externalEmptyCount = 0;
@@ -55,6 +60,10 @@
let lastDateKey = getDateKey(Date.now());
let selectedDateKey = lastDateKey;
let state = loadState();
+ let priceConfig = loadPriceConfig();
+ let lastObservedModel = "";
+ let lastObservedModelAt = 0;
+ let lastObservedModelConfidence = "unknown";
function toCount(value) {
const number = Number(value);
@@ -161,6 +170,118 @@
return { input, output, cached, reasoning, total };
}
+ function normalizeModelName(value) {
+ if (typeof value !== "string") return "";
+ const model = value.trim();
+ if (!model || model.length > 120) return "";
+ if (/^(null|undefined|default)$/i.test(model)) return "";
+ return model;
+ }
+
+ function displayModelName(model) {
+ return normalizeModelName(model) || UNKNOWN_MODEL;
+ }
+
+ function extractDirectModel(value) {
+ if (!value || typeof value !== "object") return "";
+ const candidates = [
+ value.model,
+ value.modelId,
+ value.model_id,
+ value.toModel,
+ value.threadSettings?.model,
+ value.settings?.model,
+ value.collaborationMode?.settings?.model,
+ value.params?.model,
+ value.params?.threadSettings?.model,
+ value.params?.settings?.model,
+ value.params?.collaborationMode?.settings?.model,
+ value.request?.params?.model,
+ value.request?.params?.threadSettings?.model,
+ value.request?.params?.collaborationMode?.settings?.model,
+ value.body?.model,
+ value.body?.threadSettings?.model,
+ value.body?.collaborationMode?.settings?.model,
+ ];
+ return candidates.map(normalizeModelName).find(Boolean) || "";
+ }
+
+ function parseMaybeJsonObject(value) {
+ if (!value) return null;
+ if (typeof value === "object") return value;
+ if (typeof value !== "string") return null;
+ return safeParseJson(value);
+ }
+
+ function extractModelFromAppMessage(message) {
+ if (!message || typeof message !== "object") return "";
+ const type = String(message.type || "");
+ const method = String(message.method || message.request?.method || "");
+ const params = message.params || message.request?.params || null;
+ const body = parseMaybeJsonObject(message.body);
+
+ if (type === "mcp-request" || type === "thread-prewarm-start") {
+ return extractDirectModel({ method, params, request: message.request });
+ }
+ if (
+ type === "start-conversation" ||
+ type === "start-turn-for-host" ||
+ type === "update-thread-settings-for-next-turn" ||
+ type === "thread-follower-update-thread-settings-for-host" ||
+ type === "thread-follower-start-turn-for-host" ||
+ type === "send-cli-request-for-host" ||
+ type === "prewarm-thread-start-for-host"
+ ) {
+ return extractDirectModel(message);
+ }
+ if (type === "fetch" || type === "fetch-stream") {
+ const url = String(message.url || "");
+ if (/vscode:\/\/codex\/(start-conversation|start-turn-for-host|update-thread-settings|send-cli-request|prewarm-thread-start)/.test(url)) {
+ return extractDirectModel(body || message);
+ }
+ return "";
+ }
+ if (type === "mcp-notification") {
+ if (method === "thread/settings/updated") return extractDirectModel(params?.threadSettings || params);
+ if (method === "model/rerouted") return normalizeModelName(params?.toModel) || extractDirectModel(params);
+ if (method === "thread/started") return extractDirectModel(params?.thread || params);
+ if (method === "turn/started") return extractDirectModel(params?.turn || params);
+ }
+ if (type === "thread/settings/updated") return extractDirectModel(message.threadSettings || message);
+ if (type === "model/rerouted") return normalizeModelName(message.toModel) || extractDirectModel(message);
+ if (type === "thread/started") return extractDirectModel(message.thread || message);
+ if (type === "turn/started") return extractDirectModel(message.turn || message);
+ return "";
+ }
+
+ function observeModel(model, confidence = "observed", timestamp = Date.now()) {
+ const normalized = normalizeModelName(model);
+ if (!normalized) return false;
+ lastObservedModel = normalized;
+ lastObservedModelAt = Number.isFinite(timestamp) ? timestamp : Date.now();
+ lastObservedModelConfidence = confidence;
+ return true;
+ }
+
+ function observeAppModelMessage(message, confidence = "observed") {
+ const model = extractModelFromAppMessage(message);
+ return observeModel(model, confidence);
+ }
+
+ function modelForTimestamp(timestamp = Date.now()) {
+ if (!lastObservedModel || !lastObservedModelAt) return "";
+ const time = Number.isFinite(timestamp) ? timestamp : Date.now();
+ return Math.abs(time - lastObservedModelAt) <= MODEL_BIND_WINDOW_MS ? lastObservedModel : "";
+ }
+
+ function extractTurnModel(turn, timestamp = Date.now()) {
+ const direct = extractDirectModel(turn);
+ if (direct) return { model: direct, confidence: "observed" };
+ const nearby = modelForTimestamp(timestamp);
+ if (nearby) return { model: nearby, confidence: lastObservedModelConfidence || "nearby" };
+ return { model: UNKNOWN_MODEL, confidence: "unknown" };
+ }
+
function isUsageTurn(turn) {
if (!turn || typeof turn !== "object" || !turn.turnId) return false;
const usage = normalizeUsage(turn.usage);
@@ -192,6 +313,142 @@
}
}
+ function createEmptyPriceConfig() {
+ return { version: 1, currency: "USD", models: {} };
+ }
+
+ function normalizePriceNumber(value, allowNull = true) {
+ if (value === "" || value == null) return allowNull ? null : 0;
+ const number = Number(value);
+ if (!Number.isFinite(number) || number < 0) return allowNull ? null : 0;
+ return Number(number.toFixed(6));
+ }
+
+ function normalizePriceEntry(entry) {
+ const source = entry && typeof entry === "object" ? entry : {};
+ return {
+ input: normalizePriceNumber(source.input),
+ cachedInput: normalizePriceNumber(source.cachedInput),
+ output: normalizePriceNumber(source.output),
+ reasoning: normalizePriceNumber(source.reasoning),
+ };
+ }
+
+ function isPriceEntryEmpty(entry) {
+ const normalized = normalizePriceEntry(entry);
+ return PRICE_FIELDS.every((field) => normalized[field] == null);
+ }
+
+ function loadPriceConfig() {
+ try {
+ const parsed = JSON.parse(localStorage.getItem(PRICE_STORAGE_KEY) || "null");
+ if (parsed?.version === 1 && parsed.models && typeof parsed.models === "object") {
+ const models = {};
+ for (const [model, entry] of Object.entries(parsed.models)) {
+ const normalizedModel = normalizeModelName(model);
+ if (!normalizedModel) continue;
+ const normalizedEntry = normalizePriceEntry(entry);
+ if (!isPriceEntryEmpty(normalizedEntry)) models[normalizedModel] = normalizedEntry;
+ }
+ return { version: 1, currency: "USD", models };
+ }
+ } catch {
+ // 价格配置损坏时回到空配置,避免影响主统计。
+ }
+ return createEmptyPriceConfig();
+ }
+
+ function savePriceConfig() {
+ try {
+ localStorage.setItem(PRICE_STORAGE_KEY, JSON.stringify(priceConfig));
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ function getModelPrice(model) {
+ const normalized = normalizeModelName(model);
+ return normalized ? priceConfig.models[normalized] || null : null;
+ }
+
+ function setModelPrice(model, entry) {
+ const normalized = normalizeModelName(model);
+ if (!normalized) return false;
+ const next = normalizePriceEntry(entry);
+ if (isPriceEntryEmpty(next)) delete priceConfig.models[normalized];
+ else priceConfig.models[normalized] = next;
+ savePriceConfig();
+ render();
+ return true;
+ }
+
+ function updateModelPriceField(model, field, value) {
+ if (!PRICE_FIELDS.includes(field)) return false;
+ const normalized = normalizeModelName(model);
+ if (!normalized) return false;
+ const current = normalizePriceEntry(priceConfig.models[normalized]);
+ current[field] = normalizePriceNumber(value);
+ if (isPriceEntryEmpty(current)) delete priceConfig.models[normalized];
+ else priceConfig.models[normalized] = current;
+ savePriceConfig();
+ refreshPriceDependentDisplays();
+ return true;
+ }
+
+ function clearModelPrice(model) {
+ const normalized = normalizeModelName(model);
+ if (!normalized) return false;
+ delete priceConfig.models[normalized];
+ savePriceConfig();
+ render();
+ return true;
+ }
+
+ function calculateUsageCost(usage, price) {
+ const entry = normalizePriceEntry(price);
+ const configured = PRICE_FIELDS.some((field) => entry[field] != null);
+ if (!configured) return { cost: 0, configured: false };
+
+ const input = toCount(usage?.input);
+ const cached = Math.min(input, toCount(usage?.cached));
+ const output = toCount(usage?.output);
+ const reasoning = Math.min(output, toCount(usage?.reasoning));
+ const inputRate = entry.input ?? 0;
+ const cachedRate = entry.cachedInput ?? inputRate;
+ const outputRate = entry.output ?? 0;
+ const reasoningRate = entry.reasoning ?? outputRate;
+ const billableInput = Math.max(0, input - cached);
+ const visibleOutput = Math.max(0, output - reasoning);
+ const cost =
+ (billableInput * inputRate +
+ cached * cachedRate +
+ visibleOutput * outputRate +
+ reasoning * reasoningRate) /
+ 1_000_000;
+ return { cost, configured: true };
+ }
+
+ function formatCost(value) {
+ const number = Number(value);
+ if (!Number.isFinite(number) || number <= 0) return "$0.0000";
+ const digits = number >= 1 ? 2 : number >= 0.01 ? 4 : 6;
+ return `$${number.toFixed(digits)}`;
+ }
+
+ function formatPriceInputValue(value) {
+ const number = normalizePriceNumber(value);
+ return number == null ? "" : String(number);
+ }
+
+ function refreshPriceDependentDisplays() {
+ if (!panel) return;
+ const snapshot = aggregateDay(selectedDateKey);
+ const cost = panel.querySelector('[data-field="cost"]');
+ if (cost) cost.textContent = formatCost(snapshot.cost);
+ renderModelBreakdown(snapshot);
+ }
+
function pruneState(now = Date.now()) {
const cutoff = new Date(now);
cutoff.setHours(0, 0, 0, 0);
@@ -211,6 +468,7 @@
const timestamp = getTurnTimestamp(turn);
const dateKey = getDateKey(timestamp);
const usage = normalizeUsage(turn.usage);
+ const modelMeta = extractTurnModel(turn, timestamp);
const day = state.days[dateKey] || { turns: {}, updatedAt: 0 };
const existing = day.turns[turn.turnId];
const candidate = {
@@ -222,6 +480,8 @@
calls: Math.max(1, toCount(turn.callCount)),
updatedAt: timestamp,
source: String(turn.source || "turn-aggregate"),
+ model: modelMeta.model,
+ modelConfidence: modelMeta.confidence,
};
if (existing && existing.total > candidate.total) return false;
@@ -236,6 +496,14 @@
calls: Math.max(existing.calls || 0, candidate.calls),
updatedAt: Math.max(existing.updatedAt || 0, candidate.updatedAt),
source: candidate.source,
+ model:
+ candidate.model !== UNKNOWN_MODEL || !existing.model || existing.model === UNKNOWN_MODEL
+ ? candidate.model
+ : existing.model,
+ modelConfidence:
+ candidate.model !== UNKNOWN_MODEL || !existing.model || existing.model === UNKNOWN_MODEL
+ ? candidate.modelConfidence
+ : existing.modelConfidence || "unknown",
}
: candidate;
@@ -258,7 +526,7 @@
function aggregateDay(dateKey = getDateKey(Date.now())) {
const turns = Object.values(state.days[dateKey]?.turns || {});
- return turns.reduce(
+ const summary = turns.reduce(
(summary, turn) => {
summary.input += toCount(turn.input);
summary.output += toCount(turn.output);
@@ -268,6 +536,30 @@
summary.calls += Math.max(1, toCount(turn.calls));
summary.turns += 1;
summary.updatedAt = Math.max(summary.updatedAt, toCount(turn.updatedAt));
+ const model = displayModelName(turn.model);
+ let modelSummary = summary.modelsByName[model];
+ if (!modelSummary) {
+ modelSummary = {
+ model,
+ input: 0,
+ output: 0,
+ cached: 0,
+ reasoning: 0,
+ total: 0,
+ calls: 0,
+ turns: 0,
+ cost: 0,
+ priced: false,
+ };
+ summary.modelsByName[model] = modelSummary;
+ }
+ modelSummary.input += toCount(turn.input);
+ modelSummary.output += toCount(turn.output);
+ modelSummary.cached += toCount(turn.cached);
+ modelSummary.reasoning += toCount(turn.reasoning);
+ modelSummary.total += toCount(turn.total);
+ modelSummary.calls += Math.max(1, toCount(turn.calls));
+ modelSummary.turns += 1;
return summary;
},
{
@@ -280,8 +572,26 @@
calls: 0,
turns: 0,
updatedAt: 0,
+ cost: 0,
+ pricedModels: 0,
+ modelsByName: {},
+ models: [],
}
);
+ summary.models = Object.values(summary.modelsByName)
+ .map((modelSummary) => {
+ const costInfo = calculateUsageCost(modelSummary, getModelPrice(modelSummary.model));
+ summary.cost += costInfo.cost;
+ if (costInfo.configured) summary.pricedModels += 1;
+ return {
+ ...modelSummary,
+ cost: costInfo.cost,
+ priced: costInfo.configured,
+ };
+ })
+ .sort((a, b) => b.total - a.total || a.model.localeCompare(b.model));
+ delete summary.modelsByName;
+ return summary;
}
function formatTrendDateLabel(dateKey) {
@@ -393,6 +703,8 @@
total,
calls: toCount(snapshot?.calls),
turns: toCount(snapshot?.turns),
+ cost: Number(snapshot?.cost) || 0,
+ models: Array.isArray(snapshot?.models) ? snapshot.models.slice(0, 4) : [],
cacheRate: input > 0 ? Math.min(100, (cached / input) * 100) : 0,
outputRate: total > 0 ? Math.min(100, (output / total) * 100) : 0,
trend: buildTrendData(snapshot?.dateKey || getDateKey(Date.now())),
@@ -579,6 +891,11 @@
context.fillStyle = "rgba(226, 232, 255, 0.48)";
context.font = '500 21px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
context.fillText(`${formatExact(model.turns)} 个 turn · ${formatExact(model.calls)} 次请求`, 73, 350);
+ context.textAlign = "right";
+ context.fillStyle = "rgba(114, 195, 255, 0.78)";
+ context.font = '650 22px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(`估算成本 ${formatCost(model.cost)}`, 1129, 350);
+ context.textAlign = "left";
const cardWidth = 250;
const gap = 22;
@@ -620,7 +937,11 @@
context.fillStyle = "rgba(226, 232, 255, 0.46)";
context.font = '500 19px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
- context.fillText("数据仅来自本机 Codex++,不包含会话内容", 72, 833);
+ const modelText = model.models.length
+ ? `主力 Model:${model.models.map((item) => item.model).join(" / ")}`
+ : "Model:暂无可识别数据";
+ context.fillText(modelText.slice(0, 72), 72, 809);
+ context.fillText("数据仅来自本机 Codex++,成本为本地配置价格估算,不包含会话内容", 72, 833);
context.textAlign = "right";
context.fillStyle = "rgba(114, 195, 255, 0.72)";
context.font = '650 19px -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif';
@@ -734,17 +1055,18 @@
return usage;
}
- function findUsageCandidates(value, depth = 0, inheritedId = "") {
+ function findUsageCandidates(value, depth = 0, inheritedId = "", inheritedModel = "") {
if (!value || depth > 8) return [];
if (typeof value === "string") {
- return parseTextPayloads(value).flatMap((item) => findUsageCandidates(item, depth + 1, inheritedId));
+ return parseTextPayloads(value).flatMap((item) => findUsageCandidates(item, depth + 1, inheritedId, inheritedModel));
}
if (Array.isArray(value)) {
- return value.flatMap((item) => findUsageCandidates(item, depth + 1, inheritedId));
+ return value.flatMap((item) => findUsageCandidates(item, depth + 1, inheritedId, inheritedModel));
}
if (typeof value !== "object") return [];
const id = extractCandidateId(value) || inheritedId;
+ const model = extractDirectModel(value) || inheritedModel;
const candidates = [];
const directKeys = [
"usage",
@@ -758,11 +1080,11 @@
for (const key of directKeys) {
const usage = normalizeCaptureUsage(value[key]);
- if (usage) candidates.push({ usage, id });
+ if (usage) candidates.push({ usage, id, model });
}
const selfUsage = normalizeCaptureUsage(value);
- if (selfUsage) candidates.push({ usage: selfUsage, id });
+ if (selfUsage) candidates.push({ usage: selfUsage, id, model });
for (const key of [
"response",
@@ -778,7 +1100,7 @@
"output",
"details",
]) {
- candidates.push(...findUsageCandidates(value[key], depth + 1, id));
+ candidates.push(...findUsageCandidates(value[key], depth + 1, id, model));
}
return dedupeCandidates(candidates);
@@ -819,6 +1141,7 @@
: `${now}-${++captureSeq}`;
const changed = upsertTurn({
turnId,
+ model: candidate.model,
source: `capture:${source}`,
callCount: 1,
createdAt: new Date(now).toISOString(),
@@ -842,6 +1165,7 @@
function processCapturePayload(payload, source, url = "") {
if (sourceMode === "external") return false;
+ processModelPayload(payload);
const candidates = findUsageCandidates(payload);
if (!candidates.length) return false;
let changed = false;
@@ -856,11 +1180,57 @@
return changed;
}
+ function processModelPayload(payload) {
+ if (!payload) return false;
+ if (typeof payload === "string") {
+ return parseTextPayloads(payload).some((item) => processModelPayload(item));
+ }
+ if (Array.isArray(payload)) {
+ return payload.some((item) => processModelPayload(item));
+ }
+ if (typeof payload !== "object") return false;
+
+ let observed = observeAppModelMessage(payload);
+ observed = observeModel(extractDirectModel(payload)) || observed;
+ const body = parseMaybeJsonObject(payload.body);
+ if (body && body !== payload) observed = processModelPayload(body) || observed;
+ return observed;
+ }
+
+ function installModelCapture() {
+ if (modelCaptureInstalled) return false;
+ window.addEventListener?.(
+ "codex-message-from-view",
+ (event) => {
+ try {
+ processModelPayload(event.detail);
+ } catch {
+ // 不影响 Codex 自身消息投递。
+ }
+ },
+ true
+ );
+ window.addEventListener?.(
+ "message",
+ (event) => {
+ try {
+ processModelPayload(event.data);
+ } catch {
+ // Ignore unrelated messages.
+ }
+ },
+ true
+ );
+ modelCaptureInstalled = true;
+ return true;
+ }
+
function installFetchCapture() {
if (typeof window.fetch !== "function" || window.fetch.__codexDailyTokenUsageWrapped === VERSION) return;
const originalFetch = window.fetch;
async function wrappedFetch(input, init) {
const url = requestUrl(input);
+ processModelPayload(init?.body);
const response = await originalFetch.call(this, input, init);
const contentType = String(response?.headers?.get?.("content-type") || "");
if (response?.clone && (isLikelyUsageUrl(url) || /json|event-stream|text/.test(contentType))) {
@@ -887,6 +1257,7 @@
return originalOpen.call(this, method, url, ...rest);
};
Xhr.prototype.send = function send(...args) {
+ processModelPayload(args[0]);
this.addEventListener?.("loadend", () => {
const url = this.__codexDailyTokenUsageUrl || "";
if (!isLikelyUsageUrl(url) && !String(this.getResponseHeader?.("content-type") || "").match(/json|event-stream|text/)) {
@@ -943,6 +1314,7 @@
"message",
(event) => {
try {
+ processModelPayload(event.data);
processCapturePayload(event.data, "post-message");
} catch {
// Ignore unrelated messages.
@@ -1147,6 +1519,31 @@
font-size: 13px;
font-weight: 650;
}
+ #${PANEL_ID} .codex-daily-head-actions {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ }
+ #${PANEL_ID} .codex-daily-price-toggle {
+ height: 29px;
+ display: inline-flex;
+ align-items: center;
+ padding: 0 8px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.2));
+ border-radius: 8px;
+ color: var(--color-token-foreground-secondary, #737373);
+ background: var(--color-token-background-secondary, rgba(127, 127, 127, 0.07));
+ cursor: pointer;
+ font: inherit;
+ font-size: 11px;
+ font-weight: 600;
+ }
+ #${PANEL_ID} .codex-daily-price-toggle:hover,
+ #${PANEL_ID} .codex-daily-price-toggle[aria-expanded="true"] {
+ color: var(--color-token-foreground, #202020);
+ border-color: rgba(74, 144, 226, 0.38);
+ background: rgba(74, 144, 226, 0.11);
+ }
#${PANEL_ID} .codex-daily-date-nav {
display: inline-flex;
align-items: center;
@@ -1201,6 +1598,27 @@
font-weight: 700;
font-variant-numeric: tabular-nums;
}
+ #${PANEL_ID} .codex-daily-cost {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 10px;
+ margin: -4px 0 12px;
+ padding: 9px 10px;
+ border: 1px solid rgba(74, 144, 226, 0.2);
+ border-radius: 10px;
+ background: rgba(74, 144, 226, 0.08);
+ }
+ #${PANEL_ID} .codex-daily-cost-label {
+ color: var(--color-token-foreground-secondary, #737373);
+ font-size: 11px;
+ }
+ #${PANEL_ID} .codex-daily-cost-value {
+ color: #2f7dd1;
+ font-size: 15px;
+ font-weight: 750;
+ font-variant-numeric: tabular-nums;
+ }
#${PANEL_ID} .codex-daily-grid {
display: grid;
grid-template-columns: 1fr auto;
@@ -1261,6 +1679,149 @@
color: var(--color-token-foreground, #202020);
font-weight: 650;
}
+ #${PANEL_ID} .codex-daily-models,
+ #${PANEL_ID} .codex-daily-price-panel {
+ margin-top: 11px;
+ padding: 10px 11px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.18));
+ border-radius: 10px;
+ background: var(--color-token-background-secondary, rgba(127, 127, 127, 0.055));
+ }
+ #${PANEL_ID} .codex-daily-section-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 8px;
+ font-size: 11px;
+ }
+ #${PANEL_ID} .codex-daily-section-title {
+ font-weight: 650;
+ }
+ #${PANEL_ID} .codex-daily-section-meta {
+ color: var(--color-token-foreground-secondary, #737373);
+ font-variant-numeric: tabular-nums;
+ }
+ #${PANEL_ID} .codex-daily-model-list {
+ display: grid;
+ gap: 7px;
+ }
+ #${PANEL_ID} .codex-daily-model-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 4px 10px;
+ align-items: center;
+ font-size: 11px;
+ }
+ #${PANEL_ID} .codex-daily-model-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: 600;
+ }
+ #${PANEL_ID} .codex-daily-model-cost {
+ color: var(--color-token-foreground, #202020);
+ font-weight: 650;
+ font-variant-numeric: tabular-nums;
+ }
+ #${PANEL_ID} .codex-daily-model-bar {
+ grid-column: 1 / -1;
+ height: 5px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: rgba(127, 127, 127, 0.14);
+ }
+ #${PANEL_ID} .codex-daily-model-fill {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, #3BA7FF, #8D6BFF);
+ }
+ #${PANEL_ID} .codex-daily-price-panel[hidden] {
+ display: none;
+ }
+ #${PANEL_ID} .codex-daily-price-help {
+ margin-bottom: 9px;
+ color: var(--color-token-foreground-secondary, #737373);
+ font-size: 10.5px;
+ line-height: 1.45;
+ }
+ #${PANEL_ID} .codex-daily-price-add {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 6px;
+ margin-bottom: 9px;
+ }
+ #${PANEL_ID} .codex-daily-price-model-input,
+ #${PANEL_ID} .codex-daily-price-input {
+ box-sizing: border-box;
+ min-width: 0;
+ height: 27px;
+ padding: 0 7px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.2));
+ border-radius: 7px;
+ color: var(--color-token-foreground, #202020);
+ background: var(--color-token-background, #fff);
+ font: inherit;
+ font-size: 11px;
+ outline: none;
+ }
+ #${PANEL_ID} .codex-daily-price-input {
+ width: 100%;
+ font-variant-numeric: tabular-nums;
+ }
+ #${PANEL_ID} .codex-daily-price-add-button,
+ #${PANEL_ID} .codex-daily-price-clear {
+ height: 27px;
+ padding: 0 8px;
+ border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.2));
+ border-radius: 7px;
+ color: var(--color-token-foreground, #202020);
+ background: var(--color-token-background-secondary, rgba(127, 127, 127, 0.08));
+ cursor: pointer;
+ font: inherit;
+ font-size: 11px;
+ font-weight: 600;
+ }
+ #${PANEL_ID} .codex-daily-price-list {
+ display: grid;
+ gap: 9px;
+ max-height: 260px;
+ overflow: auto;
+ padding-right: 2px;
+ }
+ #${PANEL_ID} .codex-daily-price-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 6px;
+ padding-bottom: 9px;
+ border-bottom: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.13));
+ }
+ #${PANEL_ID} .codex-daily-price-row:last-child {
+ padding-bottom: 0;
+ border-bottom: 0;
+ }
+ #${PANEL_ID} .codex-daily-price-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 11px;
+ font-weight: 650;
+ }
+ #${PANEL_ID} .codex-daily-price-grid {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 6px;
+ }
+ #${PANEL_ID} .codex-daily-price-field {
+ display: grid;
+ gap: 3px;
+ color: var(--color-token-foreground-secondary, #737373);
+ font-size: 10px;
+ }
#${PANEL_ID} .codex-daily-foot {
display: flex;
align-items: center;
@@ -1363,13 +1924,20 @@
panel.innerHTML = `
Token 用量
-
-
-
-
+
+
+
+
+
+
+
0
+
+ 估算金额
+ $0.0000
+
输入 Token0
输出 Token0
@@ -1386,6 +1954,25 @@
+
+
+ 按 Model 分布
+ 暂无定价
+
+
+
+
+
+ Model 价格设置
+ USD / 1M tokens
+
+
缓存输入留空按输入价计算;推理 Token 留空按输出价计算。价格只用于本地估算,不代表官方账单。
+
+
+
+
+
+
- 0
-
-
估算金额
-
$0.0000
+
输入 Token0
@@ -2576,6 +2620,7 @@
shiftDateKey,
clampDateKey,
getMinimumDateKey,
+ pruneState,
getTurnTimestamp,
isUsageTurn,
upsertTurn,
@@ -2596,6 +2641,9 @@
setStartedAt(value) {
startedAt = value;
},
+ getRawState() {
+ return JSON.parse(JSON.stringify(state));
+ },
replaceState(nextState) {
state = nextState;
},
From 997cc785b804dd538d8eba93ab2abb855483745c Mon Sep 17 00:00:00 2001
From: "K.T.S" <38993658@qq.com>
Date: Tue, 16 Jun 2026 14:20:08 +0800
Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=B6=8B?=
=?UTF-8?q?=E5=8A=BF=E7=82=B9=E6=82=AC=E5=81=9C=E6=98=8E=E7=BB=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.json | 8 +--
scripts/codex-daily-token-usage.js | 109 +++++++++++++++++++++++++++--
2 files changed, 109 insertions(+), 8 deletions(-)
diff --git a/index.json b/index.json
index 4487405..c13d76b 100644
--- a/index.json
+++ b/index.json
@@ -1,6 +1,6 @@
{
"version": 1,
- "updated_at": "2026-06-16T05:12:19.000Z",
+ "updated_at": "2026-06-16T06:18:38.000Z",
"scripts": [
{
"id": "codex-context-ring-restore",
@@ -71,8 +71,8 @@
{
"id": "codex-daily-token-usage",
"name": "Codex Daily Token Usage",
- "description": "每日 Token 统计,近 5 日滚动存储,自动复用已有采集,支持 Model 价格、成本估算、5 日趋势和分享图",
- "version": "1.4.2",
+ "description": "每日 Token 统计,近 5 日滚动存储,自动复用已有采集,支持 Model 价格、成本估算、趋势明细和分享图",
+ "version": "1.4.3",
"author": "kts-kris",
"tags": [
"codex",
@@ -84,7 +84,7 @@
],
"homepage": "",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-daily-token-usage.js",
- "sha256": "0ebb49b43565677f4675cc06c598fb689cdef12ebbdc5d32c85fa62d510bfb7d"
+ "sha256": "890df2074a4ef0be3d424c86a21dca1ed1b9bd9a121dea112baa16fbb242d04a"
},
{
"id": "codex-list-pagebuster",
diff --git a/scripts/codex-daily-token-usage.js b/scripts/codex-daily-token-usage.js
index bc509cb..e6e4c7d 100644
--- a/scripts/codex-daily-token-usage.js
+++ b/scripts/codex-daily-token-usage.js
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Codex Daily Token Usage
// @namespace codex-plus-plus
-// @version 1.4.2
+// @version 1.4.3
// @description 每日 Token 统计,近 5 日滚动存储,优先复用已有采集,必要时内置采集,支持 Model 价格、成本估算、日期切换、5 日趋势与分享图。
// @match app://-/*
// @run-at document-start
@@ -10,7 +10,7 @@
(() => {
"use strict";
- const VERSION = "1.4.2";
+ const VERSION = "1.4.3";
const API_KEY = "__codexDailyTokenUsage";
const SOURCE_API_KEY = "__codexTokenUsage";
const STORAGE_KEY = "__codexDailyTokenUsageV1";
@@ -703,6 +703,7 @@
input: toCount(summary.input),
output: toCount(summary.output),
calls: toCount(summary.calls),
+ cost: Number(summary.cost) || 0,
active: itemDateKey === endKey,
});
}
@@ -1762,6 +1763,7 @@
font-weight: 550;
}
#${PANEL_ID} .codex-daily-trend {
+ position: relative;
margin-top: 11px;
padding: 10px 11px 9px;
border: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.18));
@@ -1804,6 +1806,51 @@
color: var(--color-token-foreground, #202020);
font-weight: 650;
}
+ #${PANEL_ID} .codex-daily-trend-point {
+ cursor: default;
+ outline: none;
+ }
+ #${PANEL_ID} .codex-daily-trend-point:hover,
+ #${PANEL_ID} .codex-daily-trend-point:focus {
+ filter: drop-shadow(0 0 4px rgba(82, 124, 255, 0.42));
+ }
+ #${PANEL_ID} .codex-daily-trend-tooltip {
+ position: absolute;
+ z-index: 5;
+ width: max-content;
+ min-width: 158px;
+ max-width: 210px;
+ padding: 9px 10px;
+ border: 1px solid color-mix(in srgb, var(--color-token-border, rgba(127, 127, 127, 0.18)) 82%, transparent);
+ border-radius: 11px;
+ background: color-mix(in srgb, var(--color-token-background, #fff) 94%, transparent);
+ box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);
+ color: var(--color-token-foreground, #202020);
+ font-size: 11px;
+ line-height: 1.35;
+ pointer-events: none;
+ transform: translate(-50%, calc(-100% - 10px));
+ }
+ #${PANEL_ID} .codex-daily-trend-tooltip[hidden] {
+ display: none;
+ }
+ #${PANEL_ID} .codex-daily-trend-tooltip-date {
+ margin-bottom: 6px;
+ font-weight: 700;
+ }
+ #${PANEL_ID} .codex-daily-trend-tooltip-row {
+ display: flex;
+ justify-content: space-between;
+ gap: 14px;
+ margin-top: 3px;
+ color: var(--color-token-foreground-secondary, #737373);
+ }
+ #${PANEL_ID} .codex-daily-trend-tooltip-row strong {
+ color: var(--color-token-foreground, #202020);
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ }
#${PANEL_ID} .codex-daily-models,
#${PANEL_ID} .codex-daily-price-panel {
margin-top: 11px;
@@ -2083,6 +2130,7 @@
+
@@ -2432,11 +2480,56 @@
return externalSourceAvailable() ? "等待 Codex Token Usage 数据" : "等待数据源,必要时自动采集";
}
+ function trendTooltipHtml(point) {
+ return `
+
${escapeHtml(formatDisplayDate(point.dateKey))}
+
+ Token 总量
+ ${formatExact(point.total)}
+
+
+ 请求次数
+ ${formatExact(point.calls)}
+
+
+ 预估金额
+ ${formatCost(point.cost)}
+
+ `;
+ }
+
+ function hideTrendTooltip() {
+ const tooltip = panel?.querySelector(".codex-daily-trend-tooltip");
+ if (tooltip) tooltip.hidden = true;
+ }
+
+ function showTrendTooltip(point, target) {
+ if (!panel || !point || !target) return;
+ const trendBox = panel.querySelector(".codex-daily-trend");
+ const tooltip = panel.querySelector(".codex-daily-trend-tooltip");
+ const svg = panel.querySelector(".codex-daily-trend-svg");
+ if (!trendBox || !tooltip || !svg) return;
+
+ tooltip.innerHTML = trendTooltipHtml(point);
+ tooltip.hidden = false;
+
+ const boxRect = trendBox.getBoundingClientRect();
+ const svgRect = svg.getBoundingClientRect();
+ const tooltipRect = tooltip.getBoundingClientRect();
+ const x = svgRect.left - boxRect.left + (point.x / 300) * svgRect.width;
+ const y = svgRect.top - boxRect.top + (point.y / 82) * svgRect.height;
+ const minX = tooltipRect.width / 2 + 8;
+ const maxX = Math.max(minX, boxRect.width - tooltipRect.width / 2 - 8);
+ tooltip.style.left = `${Math.min(Math.max(x, minX), maxX)}px`;
+ tooltip.style.top = `${Math.max(y, tooltipRect.height + 18)}px`;
+ }
+
function renderPanelTrend(trend) {
if (!panel) return;
const svg = panel.querySelector(".codex-daily-trend-svg");
const labels = panel.querySelector(".codex-daily-trend-labels");
const peak = panel.querySelector('[data-field="trendPeak"]');
+ hideTrendTooltip();
if (peak) peak.textContent = `峰值 ${formatCompact(trend?.maxTotal || 0)}`;
const points = trendPoints(trend, 300, 82, 8);
@@ -2464,12 +2557,20 @@
${line ? `
` : ""}
${points
.map(
- (point) => `
-
+ (point, index) => `
+
`
)
.join("")}
`;
+ svg.querySelectorAll(".codex-daily-trend-point").forEach((circle) => {
+ const point = points[Number(circle.getAttribute("data-index"))];
+ circle.addEventListener("pointerenter", () => showTrendTooltip(point, circle));
+ circle.addEventListener("pointermove", () => showTrendTooltip(point, circle));
+ circle.addEventListener("focus", () => showTrendTooltip(point, circle));
+ circle.addEventListener("pointerleave", hideTrendTooltip);
+ circle.addEventListener("blur", hideTrendTooltip);
+ });
}
if (labels) {