From cf402d55e0530c026aabeb8bce87085e4bccfb20 Mon Sep 17 00:00:00 2001
From: "K.T.S" <38993658@qq.com>
Date: Tue, 7 Jul 2026 15:36:13 +0800
Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E6=AF=8F?=
=?UTF-8?q?=E6=97=A5=E7=94=A8=E9=87=8F=E5=B7=A5=E5=85=B7=E6=B4=9E=E5=AF=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.json | 4 +-
scripts/codex-daily-token-usage.js | 1018 +++++++++++++++++++++++++++-
2 files changed, 994 insertions(+), 28 deletions(-)
diff --git a/index.json b/index.json
index 5fba5f4..a105795 100644
--- a/index.json
+++ b/index.json
@@ -1,6 +1,6 @@
{
"version": 1,
- "updated_at": "2026-07-06T08:45:38.039Z",
+ "updated_at": "2026-07-07T07:45:09.000Z",
"scripts": [
{
"id": "codex-context-used-meter",
@@ -67,7 +67,7 @@
],
"homepage": "",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-daily-token-usage.js",
- "sha256": "660499e55ac2f4b6cafd32403a584d9a8b0b6d761c5e68d27d793611df4af3b7"
+ "sha256": "daa394c603bc67ff5a3e52930bad7f41d7cf3e4a750883aedc2e7f3397402814"
},
{
"id": "codex-list-pagebuster",
diff --git a/scripts/codex-daily-token-usage.js b/scripts/codex-daily-token-usage.js
index c74ccaa..31e7f22 100644
--- a/scripts/codex-daily-token-usage.js
+++ b/scripts/codex-daily-token-usage.js
@@ -25,7 +25,12 @@
const POLL_INTERVAL_MS = 1000;
const RETAIN_DAYS = 5;
const MAX_TURNS_PER_DAY = 2000;
+ const MAX_TOOL_CALLS_PER_DAY = 500;
+ const MAX_TOOL_CALL_EVENT_KEYS_PER_DAY = 3000;
+ const MAX_TOOL_CALL_EVENTS_PER_DAY = 3000;
+ const TOOL_USAGE_MATCH_WINDOW_MS = 15 * 60 * 1000;
const CAPTURE_DEDUPE_WINDOW_MS = 3000;
+ const TOOL_CALL_DEDUPE_WINDOW_MS = 3000;
const MAX_CAPTURE_BODY_CHARS = 2_000_000;
const EXTERNAL_EMPTY_LIMIT = 4;
const TREND_DAYS = 5;
@@ -42,6 +47,9 @@
const PANEL_GAP = 8;
const PANEL_MARGIN = 12;
const WINDOW_BUTTON_SAFE_RIGHT = 132;
+ const DOM_TOOL_DESCRIPTORS = [
+ { selector: '[data-testid="exec-shell-body"]', testId: "exec-shell-body", kind: "plugin", name: "exec_command" },
+ ];
const previous = window[API_KEY];
if (previous && typeof previous.destroy === "function") {
@@ -434,6 +442,82 @@
return { model: UNKNOWN_MODEL, confidence: "unknown" };
}
+ function normalizeToolName(value) {
+ const text = String(value ?? "").trim();
+ if (!text || /^(none|null|undefined)$/i.test(text)) return "";
+ return text.replace(/\s+/g, " ").slice(0, 160);
+ }
+
+ function normalizeToolNamespace(value) {
+ const text = String(value ?? "").trim();
+ if (!text || /^(none|null|undefined)$/i.test(text)) return "";
+ return text.replace(/\s+/g, " ").slice(0, 120);
+ }
+
+ function parseMcpToolName(name) {
+ const normalized = normalizeToolName(name);
+ if (!normalized.startsWith("mcp__")) return null;
+ const parts = normalized.split("__").filter(Boolean);
+ if (parts.length < 3 || parts[0] !== "mcp") return null;
+ return {
+ namespace: normalizeToolNamespace(parts[1]),
+ name: normalizeToolName(parts.slice(2).join("__")),
+ };
+ }
+
+ function classifyToolKind(name, namespace, kind = "") {
+ const text = `${kind} ${namespace || ""} ${name || ""}`.toLowerCase();
+ if (text.includes("mcp") || String(name || "").startsWith("mcp__")) return "mcp";
+ return "plugin";
+ }
+
+ function normalizeToolEvent(event) {
+ if (!event || typeof event !== "object") return null;
+ const parsedMcpName = parseMcpToolName(event.name);
+ const rawName = parsedMcpName?.name || event.name;
+ const name = normalizeToolName(rawName);
+ if (!name) return null;
+
+ const namespace = normalizeToolNamespace(event.namespace || parsedMcpName?.namespace);
+ const kind = classifyToolKind(name, namespace, event.kind || (parsedMcpName ? "mcp" : ""));
+ return {
+ kind,
+ name,
+ namespace,
+ id: normalizeToolName(event.id),
+ source: normalizeToolName(event.source),
+ timestamp: parseTimestamp(event.timestamp) || Date.now(),
+ turnKey: normalizeConversationKey(event.turnKey || event.turn_id || event.turnId),
+ conversationKey: normalizeConversationKey(event.conversationKey || event.conversationId || event.conversation_id),
+ };
+ }
+
+ function toolCallKey(kind, namespace, name) {
+ return [kind || "plugin", namespace || "", name || ""].join("|").toLowerCase();
+ }
+
+ function toolCallEventKey(event) {
+ if (!event?.id) return "";
+ return `${event.kind || "plugin"}|${event.namespace || ""}|${event.name || ""}|id:${event.id}`.toLowerCase();
+ }
+
+ function displayToolKind(kind) {
+ return kind === "mcp" ? "MCP" : "插件";
+ }
+
+ function compactToolEvent(event, timestamp) {
+ return {
+ kind: event.kind,
+ name: event.name,
+ namespace: event.namespace,
+ id: event.id,
+ source: event.source || "capture",
+ timestamp,
+ turnKey: event.turnKey || "",
+ conversationKey: event.conversationKey || "",
+ };
+ }
+
function isUsageTurn(turn) {
if (!turn || typeof turn !== "object" || !turn.turnId) return false;
const usage = normalizeUsage(turn.usage);
@@ -692,8 +776,133 @@
return true;
}
+ function recordToolCall(rawEvent, timestamp = Date.now()) {
+ const event = normalizeToolEvent({ ...rawEvent, timestamp: rawEvent?.timestamp || timestamp });
+ if (!event) return false;
+
+ const now = Number.isFinite(event.timestamp) ? event.timestamp : Date.now();
+ cleanupRecentToolCallKeys(now);
+ const persistedEventKey = toolCallEventKey(event);
+ const dedupeKey =
+ persistedEventKey ||
+ `${event.kind}|${event.namespace}|${event.name}|${event.source}|${Math.floor(now / TOOL_CALL_DEDUPE_WINDOW_MS)}`;
+ if (recentToolCallKeys.has(dedupeKey)) return false;
+ recentToolCallKeys.set(dedupeKey, now);
+
+ const dateKey = getDateKey(now);
+ const day = state.days[dateKey] || { turns: {}, updatedAt: 0 };
+ if (!day.turns || typeof day.turns !== "object") day.turns = {};
+ if (!day.toolCalls || typeof day.toolCalls !== "object") day.toolCalls = {};
+ if (!day.toolCallEventKeys || typeof day.toolCallEventKeys !== "object") day.toolCallEventKeys = {};
+ if (!day.toolEvents || typeof day.toolEvents !== "object") day.toolEvents = {};
+ if (persistedEventKey && day.toolCallEventKeys[persistedEventKey]) {
+ if (!day.toolEvents[persistedEventKey]) {
+ day.toolEvents[persistedEventKey] = compactToolEvent(event, now);
+ state.days[dateKey] = day;
+ return true;
+ }
+ return false;
+ }
+
+ const key = toolCallKey(event.kind, event.namespace, event.name);
+ const existing = day.toolCalls[key] || {};
+ day.toolCalls[key] = {
+ kind: event.kind,
+ name: event.name,
+ namespace: event.namespace,
+ count: toCount(existing.count) + 1,
+ lastCalledAt: Math.max(toCount(existing.lastCalledAt), now),
+ source: event.source || existing.source || "capture",
+ };
+ day.updatedAt = Math.max(day.updatedAt || 0, now);
+ if (persistedEventKey) day.toolCallEventKeys[persistedEventKey] = now;
+ if (persistedEventKey) day.toolEvents[persistedEventKey] = compactToolEvent(event, now);
+
+ const keys = Object.keys(day.toolCalls);
+ if (keys.length > MAX_TOOL_CALLS_PER_DAY) {
+ keys
+ .sort((a, b) => (day.toolCalls[a].lastCalledAt || 0) - (day.toolCalls[b].lastCalledAt || 0))
+ .slice(0, keys.length - MAX_TOOL_CALLS_PER_DAY)
+ .forEach((id) => delete day.toolCalls[id]);
+ }
+ const eventKeys = Object.keys(day.toolCallEventKeys);
+ if (eventKeys.length > MAX_TOOL_CALL_EVENT_KEYS_PER_DAY) {
+ eventKeys
+ .sort((a, b) => toCount(day.toolCallEventKeys[a]) - toCount(day.toolCallEventKeys[b]))
+ .slice(0, eventKeys.length - MAX_TOOL_CALL_EVENT_KEYS_PER_DAY)
+ .forEach((id) => {
+ delete day.toolCallEventKeys[id];
+ delete day.toolEvents[id];
+ });
+ }
+ const toolEventKeys = Object.keys(day.toolEvents);
+ if (toolEventKeys.length > MAX_TOOL_CALL_EVENTS_PER_DAY) {
+ toolEventKeys
+ .sort((a, b) => toCount(day.toolEvents[a]?.timestamp) - toCount(day.toolEvents[b]?.timestamp))
+ .slice(0, toolEventKeys.length - MAX_TOOL_CALL_EVENTS_PER_DAY)
+ .forEach((id) => delete day.toolEvents[id]);
+ }
+
+ state.days[dateKey] = day;
+ return true;
+ }
+
+ function normalizeStoredToolEvent(value, storageKey = "") {
+ const event = normalizeToolEvent(value);
+ if (!event) return null;
+ return {
+ ...event,
+ storageKey,
+ turnKey: normalizeConversationKey(value?.turnKey || event.turnKey),
+ conversationKey: normalizeConversationKey(value?.conversationKey || event.conversationKey),
+ };
+ }
+
+ function turnIdentityKeys(id, turn) {
+ return conversationKeyVariants(id)
+ .concat(
+ conversationKeyVariants(turn?.turnId),
+ conversationKeyVariants(turn?.turnKey),
+ conversationKeyVariants(turn?.id)
+ )
+ .filter(Boolean);
+ }
+
+ function findExactToolTurnId(event, turnEntries) {
+ const keys = conversationKeyVariants(event.turnKey);
+ if (!keys.length) return "";
+ for (const [id, turn] of turnEntries) {
+ const turnKeys = new Set(turnIdentityKeys(id, turn));
+ if (keys.some((key) => turnKeys.has(key))) return id;
+ }
+ return "";
+ }
+
+ function findNearestToolTurnId(event, turnEntries) {
+ const timestamp = parseTimestamp(event.timestamp);
+ if (!timestamp || event.source === "dom-tool-card") return "";
+ let best = null;
+ for (const [id, turn] of turnEntries) {
+ const updatedAt = toCount(turn?.updatedAt);
+ if (!updatedAt) continue;
+ const distance = Math.abs(updatedAt - timestamp);
+ const isAfterTool = updatedAt >= timestamp;
+ if (distance > TOOL_USAGE_MATCH_WINDOW_MS) continue;
+ const score = distance + (isAfterTool ? 0 : TOOL_USAGE_MATCH_WINDOW_MS / 2);
+ if (!best || score < best.score) best = { id, score };
+ }
+ return best?.id || "";
+ }
+
+ function calculateTurnCost(turn) {
+ const costInfo = calculateUsageCost(turn, getModelPrice(displayModelName(turn?.model)));
+ return costInfo.configured ? costInfo.cost : 0;
+ }
+
function aggregateDay(dateKey = getDateKey(Date.now())) {
- const turns = Object.values(state.days[dateKey]?.turns || {});
+ const day = state.days[dateKey] || {};
+ const turnEntries = Object.entries(day.turns || {});
+ const turns = turnEntries.map(([, turn]) => turn);
const summary = turns.reduce(
(summary, turn) => {
summary.input += toCount(turn.input);
@@ -744,6 +953,20 @@
pricedModels: 0,
modelsByName: {},
models: [],
+ toolCallsByKey: {},
+ toolCalls: [],
+ mcpCalls: 0,
+ pluginCalls: 0,
+ toolCallTotal: 0,
+ toolTurnCount: 0,
+ toolCallRate: 0,
+ avgToolsPerToolTurn: 0,
+ toolsPer100kTokens: 0,
+ toolLinkedTurns: 0,
+ toolLinkedTokens: 0,
+ toolLinkedCost: 0,
+ toolLinkedCoverage: 0,
+ toolLinkedEstimated: false,
}
);
summary.models = Object.values(summary.modelsByName)
@@ -758,7 +981,78 @@
};
})
.sort((a, b) => b.total - a.total || a.model.localeCompare(b.model));
+
+ const toolEvents = Object.entries(day.toolEvents || {})
+ .map(([key, value]) => normalizeStoredToolEvent(value, key))
+ .filter(Boolean);
+ const toolTurnsByKey = new Map();
+ const toolTurnKeys = new Set();
+ const matchedTurnIds = new Set();
+ for (const event of toolEvents) {
+ const key = toolCallKey(event.kind, event.namespace, event.name);
+ const turnKey = normalizeConversationKey(event.turnKey);
+ if (turnKey) {
+ toolTurnKeys.add(turnKey);
+ if (!toolTurnsByKey.has(key)) toolTurnsByKey.set(key, new Set());
+ toolTurnsByKey.get(key).add(turnKey);
+ }
+
+ const exactTurnId = findExactToolTurnId(event, turnEntries);
+ const matchedTurnId = exactTurnId || findNearestToolTurnId(event, turnEntries);
+ if (matchedTurnId) {
+ matchedTurnIds.add(matchedTurnId);
+ if (!exactTurnId) summary.toolLinkedEstimated = true;
+ }
+ }
+
+ for (const toolCall of Object.values(day.toolCalls || {})) {
+ const name = normalizeToolName(toolCall.name);
+ if (!name) continue;
+ const namespace = normalizeToolNamespace(toolCall.namespace);
+ const kind = classifyToolKind(name, namespace, toolCall.kind);
+ const key = toolCallKey(kind, namespace, name);
+ const existing =
+ summary.toolCallsByKey[key] ||
+ {
+ kind,
+ name,
+ namespace,
+ count: 0,
+ lastCalledAt: 0,
+ source: "",
+ turns: 0,
+ };
+ existing.count += Math.max(1, toCount(toolCall.count));
+ existing.lastCalledAt = Math.max(existing.lastCalledAt, toCount(toolCall.lastCalledAt));
+ existing.source = toolCall.source || existing.source;
+ existing.turns = Math.max(existing.turns, toolTurnsByKey.get(key)?.size || 0);
+ summary.toolCallsByKey[key] = existing;
+ summary.updatedAt = Math.max(summary.updatedAt, existing.lastCalledAt);
+ if (kind === "mcp") summary.mcpCalls += Math.max(1, toCount(toolCall.count));
+ else summary.pluginCalls += Math.max(1, toCount(toolCall.count));
+ }
+ summary.toolCallTotal = summary.mcpCalls + summary.pluginCalls;
+ summary.toolTurnCount = toolTurnKeys.size;
+ summary.toolCallRate = summary.turns > 0 ? summary.toolTurnCount / summary.turns : 0;
+ summary.avgToolsPerToolTurn = summary.toolTurnCount > 0 ? summary.toolCallTotal / summary.toolTurnCount : 0;
+ summary.toolsPer100kTokens = summary.total > 0 ? (summary.toolCallTotal / summary.total) * 100000 : 0;
+ for (const turnId of matchedTurnIds) {
+ const turn = day.turns?.[turnId];
+ if (!turn) continue;
+ summary.toolLinkedTurns += 1;
+ summary.toolLinkedTokens += toCount(turn.total);
+ summary.toolLinkedCost += calculateTurnCost(turn);
+ }
+ summary.toolLinkedCoverage = summary.toolTurnCount > 0 ? summary.toolLinkedTurns / summary.toolTurnCount : 0;
+ summary.toolCalls = Object.values(summary.toolCallsByKey).sort(
+ (a, b) =>
+ b.count - a.count ||
+ displayToolKind(a.kind).localeCompare(displayToolKind(b.kind)) ||
+ a.name.localeCompare(b.name)
+ );
+
delete summary.modelsByName;
+ delete summary.toolCallsByKey;
return summary;
}
@@ -836,6 +1130,19 @@
return new Intl.NumberFormat("zh-CN").format(toCount(value));
}
+ function formatDecimal(value, digits = 1) {
+ const number = Number(value);
+ if (!Number.isFinite(number) || number <= 0) return "0";
+ return number.toFixed(digits).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
+ }
+
+ function formatPercent(value) {
+ const number = Number(value);
+ if (!Number.isFinite(number) || number <= 0) return "0%";
+ const percent = Math.min(100, number * 100);
+ return `${formatDecimal(percent, percent >= 10 ? 1 : 2)}%`;
+ }
+
function formatTime(timestamp) {
if (!timestamp) return "暂无";
return new Intl.DateTimeFormat("zh-CN", {
@@ -874,6 +1181,17 @@
turns: toCount(snapshot?.turns),
cost: Number(snapshot?.cost) || 0,
models: Array.isArray(snapshot?.models) ? snapshot.models.slice(0, 4) : [],
+ toolCalls: Array.isArray(snapshot?.toolCalls) ? snapshot.toolCalls.slice(0, 4) : [],
+ mcpCalls: toCount(snapshot?.mcpCalls),
+ pluginCalls: toCount(snapshot?.pluginCalls),
+ toolCallTotal: toCount(snapshot?.toolCallTotal),
+ toolTurnCount: toCount(snapshot?.toolTurnCount),
+ toolCallRate: Number(snapshot?.toolCallRate) || 0,
+ avgToolsPerToolTurn: Number(snapshot?.avgToolsPerToolTurn) || 0,
+ toolsPer100kTokens: Number(snapshot?.toolsPer100kTokens) || 0,
+ toolLinkedTurns: toCount(snapshot?.toolLinkedTurns),
+ toolLinkedTokens: toCount(snapshot?.toolLinkedTokens),
+ toolLinkedCost: Number(snapshot?.toolLinkedCost) || 0,
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())),
@@ -918,6 +1236,20 @@
context.fillText(value, x + 22, y + 112);
}
+ function truncateCanvasText(context, text, maxWidth) {
+ const source = String(text || "");
+ if (!source || context.measureText(source).width <= maxWidth) return source;
+ const ellipsis = "…";
+ let low = 0;
+ let high = source.length;
+ while (low < high) {
+ const mid = Math.ceil((low + high) / 2);
+ if (context.measureText(`${source.slice(0, mid)}${ellipsis}`).width <= maxWidth) low = mid;
+ else high = mid - 1;
+ }
+ return `${source.slice(0, Math.max(0, low))}${ellipsis}`;
+ }
+
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)";
@@ -993,20 +1325,89 @@
context.textAlign = "left";
}
+ function drawShareTools(context, model, x, y, width, height) {
+ fillRoundedRect(context, x, y, width, height, 28, "rgba(255, 255, 255, 0.07)");
+ context.strokeStyle = "rgba(255, 255, 255, 0.12)";
+ 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("工具调用", x + 30, y + 43);
+
+ context.textAlign = "right";
+ context.fillStyle = "rgba(226, 232, 255, 0.58)";
+ context.font = '500 20px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(
+ `MCP ${formatExact(model.mcpCalls)} · 插件 ${formatExact(model.pluginCalls)}`,
+ x + width - 30,
+ y + 43
+ );
+ context.textAlign = "left";
+
+ const metricY = y + 68;
+ const metricHeight = 66;
+ const metricWidth = (width - 60 - 36) / 4;
+ const metrics = [
+ { label: "总调用", value: `${formatExact(model.toolCallTotal)} 次`, accent: "#4FB6FF" },
+ { label: "可识别请求", value: `${formatExact(model.toolTurnCount)} 个`, accent: "#9D7CFF" },
+ { label: "调用率", value: formatPercent(model.toolCallRate), accent: "#4EE4B1" },
+ { label: "调用密度", value: `${formatDecimal(model.toolsPer100kTokens, 2)}/10万T`, accent: "#FFB35A" },
+ ];
+ metrics.forEach((metric, index) => {
+ const metricX = x + 30 + index * (metricWidth + 12);
+ fillRoundedRect(context, metricX, metricY, metricWidth, metricHeight, 16, "rgba(5, 8, 24, 0.32)");
+ fillRoundedRect(context, metricX + 14, metricY + 16, 8, 8, 4, metric.accent);
+ context.fillStyle = "rgba(226, 232, 255, 0.54)";
+ context.font = '500 17px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(metric.label, metricX + 29, metricY + 25);
+ context.fillStyle = "#ffffff";
+ context.font = '700 20px -apple-system, BlinkMacSystemFont, "SF Pro Display", sans-serif';
+ context.fillText(metric.value, metricX + 14, metricY + 53);
+ });
+
+ const topTool = model.toolCalls?.[0];
+ const topToolName = topTool
+ ? `${topTool.namespace ? `${topTool.namespace} / ` : ""}${topTool.name}`
+ : "暂无可识别工具";
+ const topToolText = topTool
+ ? `Top:${topToolName} · ${formatExact(topTool.count)} 次${topTool.turns ? ` / ${formatExact(topTool.turns)} 个请求` : ""}`
+ : "Top:暂无可识别工具调用";
+ context.fillStyle = "rgba(226, 232, 255, 0.5)";
+ context.font = '500 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(
+ truncateCanvasText(context, topToolText, model.toolLinkedTurns ? width - 490 : width - 60),
+ x + 30,
+ y + height - 30
+ );
+
+ if (model.toolLinkedTurns) {
+ context.textAlign = "right";
+ context.fillStyle = "rgba(114, 195, 255, 0.72)";
+ context.font = '650 18px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
+ context.fillText(
+ `关联用量 ${formatCompact(model.toolLinkedTokens)} · ${formatCost(model.toolLinkedCost)}`,
+ x + width - 30,
+ y + height - 30
+ );
+ context.textAlign = "left";
+ }
+ }
+
function createShareCanvas(dateKey = selectedDateKey) {
const model = buildShareModel(aggregateDay(clampDateKey(dateKey)));
const canvas = document.createElement("canvas");
canvas.width = 1200;
- canvas.height = 900;
+ canvas.height = 1100;
const context = canvas.getContext("2d");
if (!context) throw new Error("当前环境不支持 Canvas 2D");
- const background = context.createLinearGradient(0, 0, 1200, 900);
+ const background = context.createLinearGradient(0, 0, 1200, 1100);
background.addColorStop(0, "#070A18");
background.addColorStop(0.5, "#11132D");
background.addColorStop(1, "#21104A");
context.fillStyle = background;
- context.fillRect(0, 0, 1200, 900);
+ context.fillRect(0, 0, 1200, 1100);
const blueGlow = context.createRadialGradient(160, 120, 0, 160, 120, 430);
blueGlow.addColorStop(0, "rgba(35, 160, 255, 0.34)");
@@ -1018,11 +1419,11 @@
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.fillRect(500, 300, 700, 800);
context.fillStyle = "rgba(255, 255, 255, 0.035)";
for (let x = 40; x < 1200; x += 42) {
- for (let y = 38; y < 900; y += 42) {
+ for (let y = 38; y < 1100; y += 42) {
context.beginPath();
context.arc(x, y, 1.5, 0, Math.PI * 2);
context.fill();
@@ -1103,18 +1504,23 @@
});
drawShareTrend(context, model.trend, 70, 590, 1066, 188);
+ drawShareTools(context, model, 70, 800, 1066, 188);
context.fillStyle = "rgba(226, 232, 255, 0.46)";
context.font = '500 19px -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif';
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.fillText(truncateCanvasText(context, modelText, 840), 72, 1034);
+ context.fillText(
+ truncateCanvasText(context, "数据仅来自本机 Codex++,成本为本地配置价格估算,不包含会话内容", 900),
+ 72,
+ 1062
+ );
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.fillText("GENERATED LOCALLY", 1129, 1062);
context.textAlign = "left";
return canvas;
@@ -1147,6 +1553,7 @@
}
const recentCaptureKeys = new Map();
+ const recentToolCallKeys = new Map();
function cleanupRecentCaptureKeys(now = Date.now()) {
for (const [key, timestamp] of recentCaptureKeys) {
@@ -1156,6 +1563,14 @@
}
}
+ function cleanupRecentToolCallKeys(now = Date.now()) {
+ for (const [key, timestamp] of recentToolCallKeys) {
+ if (now - timestamp > TOOL_CALL_DEDUPE_WINDOW_MS * 4) {
+ recentToolCallKeys.delete(key);
+ }
+ }
+ }
+
function requestUrl(input) {
if (typeof input === "string") return input;
if (input?.url) return String(input.url);
@@ -1298,6 +1713,247 @@
});
}
+ function firstToolName(...values) {
+ for (const value of values) {
+ if (value && typeof value === "object") {
+ const nested = firstToolName(
+ value.name,
+ value.tool,
+ value.toolName,
+ value.tool_name,
+ value.function?.name,
+ value.item?.name
+ );
+ if (nested) return nested;
+ continue;
+ }
+ const name = normalizeToolName(value);
+ if (name) return name;
+ }
+ return "";
+ }
+
+ function firstToolNamespace(...values) {
+ for (const value of values) {
+ if (value && typeof value === "object") {
+ const nested = firstToolNamespace(
+ value.namespace,
+ value.server,
+ value.serverName,
+ value.server_name,
+ value.mcpServer,
+ value.mcp_server
+ );
+ if (nested) return nested;
+ continue;
+ }
+ const namespace = normalizeToolNamespace(value);
+ if (namespace) return namespace;
+ }
+ return "";
+ }
+
+ function extractToolCallId(value) {
+ if (!value || typeof value !== "object") return "";
+ return firstToolName(
+ value.call_id,
+ value.callId,
+ value.tool_call_id,
+ value.toolCallId,
+ value.item_id,
+ value.itemId,
+ value.id,
+ value.event_id,
+ value.eventId,
+ value.request_id,
+ value.requestId,
+ value.response_id,
+ value.responseId,
+ value.item?.call_id,
+ value.item?.id,
+ value.function?.id
+ );
+ }
+
+ function extractToolCallTimestamp(value) {
+ if (!value || typeof value !== "object") return null;
+ return parseTimestamp(
+ value.timestamp ??
+ value.created_at ??
+ value.createdAt ??
+ value.updated_at ??
+ value.updatedAt ??
+ value.item?.created_at ??
+ value.item?.createdAt
+ );
+ }
+
+ function extractToolTurnKey(value) {
+ if (!value || typeof value !== "object") return "";
+ return normalizeConversationKey(
+ value.turnKey ??
+ value.turn_key ??
+ value.turnId ??
+ value.turn_id ??
+ value.internal_chat_message_metadata_passthrough?.turn_id ??
+ value.internalChatMessageMetadataPassthrough?.turnId ??
+ value.item?.turnKey ??
+ value.item?.turn_id ??
+ value.item?.internal_chat_message_metadata_passthrough?.turn_id
+ );
+ }
+
+ function isMcpToolCallMethod(method) {
+ const text = String(method || "").toLowerCase();
+ return /(^|\/)(tools\/call|tool\/call|mcp\/tool\/call)$/.test(text) || text.includes("tools/call");
+ }
+
+ function mcpRequestParams(value) {
+ if (!value || typeof value !== "object") return {};
+ const request = value.request && typeof value.request === "object" ? value.request : {};
+ const body = parseMaybeJsonObject(value.body);
+ if (value.params && typeof value.params === "object") return value.params;
+ if (request.params && typeof request.params === "object") return request.params;
+ if (body?.params && typeof body.params === "object") return body.params;
+ return {};
+ }
+
+ function extractMcpToolCallEvent(value) {
+ if (!value || typeof value !== "object") return null;
+ const method = String(value.method || value.request?.method || "");
+ if (!isMcpToolCallMethod(method)) return null;
+ const params = mcpRequestParams(value);
+ const name = firstToolName(params, value);
+ if (!name) return null;
+ return {
+ kind: "mcp",
+ name,
+ namespace: firstToolNamespace(params, value),
+ id: extractToolCallId(value) || extractToolCallId(value.request) || extractToolCallId(params),
+ source: "mcp-request",
+ timestamp: extractToolCallTimestamp(value),
+ turnKey: extractToolTurnKey(value) || extractToolTurnKey(value.request) || extractToolTurnKey(params),
+ conversationKey: extractConversationKey(value),
+ };
+ }
+
+ function isFunctionToolItem(value) {
+ if (!value || typeof value !== "object") return false;
+ const type = String(value.type || "").toLowerCase();
+ return (
+ type === "function_call" ||
+ type === "custom_tool_call" ||
+ type === "tool_call" ||
+ Boolean(value.function?.name)
+ );
+ }
+
+ function isToolCallDoneEvent(type) {
+ return /^(response\.)?output_item\.(done|completed)$/.test(String(type || "").toLowerCase());
+ }
+
+ function isToolCallPreviewEvent(type) {
+ return /(output_item\.added|function_call_arguments\.delta|tool_call.*delta)$/i.test(String(type || ""));
+ }
+
+ function functionToolCallEvent(value, source = "function-call") {
+ if (!isFunctionToolItem(value)) return null;
+ const name = firstToolName(value);
+ if (!name) return null;
+ return {
+ kind: classifyToolKind(name, firstToolNamespace(value), value.kind),
+ name,
+ namespace: firstToolNamespace(value),
+ id: extractToolCallId(value),
+ source,
+ timestamp: extractToolCallTimestamp(value),
+ turnKey: extractToolTurnKey(value),
+ conversationKey: extractConversationKey(value),
+ };
+ }
+
+ function dedupeToolCallEvents(events) {
+ const seen = new Set();
+ const normalizedEvents = [];
+ for (const event of events) {
+ const normalized = normalizeToolEvent(event);
+ if (!normalized) continue;
+ const key = normalized.id
+ ? `${normalized.kind}|${normalized.namespace}|${normalized.name}|id:${normalized.id}`
+ : `${normalized.kind}|${normalized.namespace}|${normalized.name}|${normalized.source || ""}`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ normalizedEvents.push(normalized);
+ }
+ return normalizedEvents;
+ }
+
+ function extractToolCallEvents(value, depth = 0) {
+ if (!value || depth > 8) return [];
+ if (typeof value === "string") {
+ return dedupeToolCallEvents(parseTextPayloads(value).flatMap((item) => extractToolCallEvents(item, depth + 1)));
+ }
+ if (Array.isArray(value)) {
+ return dedupeToolCallEvents(value.flatMap((item) => extractToolCallEvents(item, depth + 1)));
+ }
+ if (typeof value !== "object") return [];
+
+ const events = [];
+ const type = String(value.type || "");
+ const mcpEvent = extractMcpToolCallEvent(value);
+ if (mcpEvent) events.push(mcpEvent);
+
+ if (isToolCallDoneEvent(type)) {
+ const doneEvent = functionToolCallEvent(value.item || value.output_item || value, type || "output-item-done");
+ if (doneEvent) events.push(doneEvent);
+ } else if (!isToolCallPreviewEvent(type)) {
+ const directEvent = functionToolCallEvent(value, "function-call");
+ if (directEvent) events.push(directEvent);
+ }
+
+ const toolCalls = [
+ ...(Array.isArray(value.tool_calls) ? value.tool_calls : []),
+ ...(Array.isArray(value.toolCalls) ? value.toolCalls : []),
+ ];
+ for (const call of toolCalls) {
+ const toolCallEvent = functionToolCallEvent(call, "tool-calls");
+ if (toolCallEvent) events.push(toolCallEvent);
+ }
+
+ if (!isToolCallPreviewEvent(type)) {
+ for (const key of [
+ "response",
+ "data",
+ "body",
+ "message",
+ "result",
+ "event",
+ "params",
+ "payload",
+ "item",
+ "output",
+ "choices",
+ "delta",
+ ]) {
+ let nestedValue = value[key];
+ if (nestedValue && typeof nestedValue === "object" && !Array.isArray(nestedValue)) {
+ const inherited = {};
+ if (value.timestamp && !nestedValue.timestamp) inherited.timestamp = value.timestamp;
+ const parentTurnKey = extractToolTurnKey(value);
+ if (parentTurnKey && !extractToolTurnKey(nestedValue)) inherited.turnKey = parentTurnKey;
+ const parentConversationKey = extractConversationKey(value);
+ if (parentConversationKey && !extractConversationKey(nestedValue)) {
+ inherited.conversationId = parentConversationKey;
+ }
+ if (Object.keys(inherited).length) nestedValue = { ...nestedValue, ...inherited };
+ }
+ events.push(...extractToolCallEvents(nestedValue, depth + 1));
+ }
+ }
+
+ return dedupeToolCallEvents(events);
+ }
+
function rememberCapturedUsage(candidate, source, url = "") {
const now = Date.now();
cleanupRecentCaptureKeys(now);
@@ -1335,11 +1991,152 @@
return changed;
}
+ function processToolCallPayload(payload, source = "capture") {
+ const events = extractToolCallEvents(payload).map((event) => ({
+ ...event,
+ source: event.source || source,
+ }));
+ if (!events.length) return false;
+
+ let changed = false;
+ for (const event of events) {
+ changed = recordToolCall(event) || changed;
+ }
+ if (changed) {
+ pruneState();
+ saveState();
+ render({ animate: false });
+ }
+ return changed;
+ }
+
+ function safeQuery(rootNode, selector) {
+ try {
+ return rootNode && typeof rootNode.querySelector === "function" ? rootNode.querySelector(selector) : null;
+ } catch {
+ return null;
+ }
+ }
+
+ function safeQueryAll(rootNode, selector) {
+ try {
+ return rootNode && typeof rootNode.querySelectorAll === "function"
+ ? Array.from(rootNode.querySelectorAll(selector))
+ : [];
+ } catch {
+ return [];
+ }
+ }
+
+ function elementAttribute(element, name) {
+ try {
+ return typeof element?.getAttribute === "function" ? element.getAttribute(name) || "" : "";
+ } catch {
+ return "";
+ }
+ }
+
+ function closestElement(element, selector) {
+ try {
+ return typeof element?.closest === "function" ? element.closest(selector) : null;
+ } catch {
+ return null;
+ }
+ }
+
+ function shortHash(value) {
+ let hash = 2166136261;
+ const text = String(value || "");
+ for (let index = 0; index < text.length; index += 1) {
+ hash ^= text.charCodeAt(index);
+ hash = Math.imul(hash, 16777619);
+ }
+ return (hash >>> 0).toString(36);
+ }
+
+ function currentConversationIdFromDom(rootNode = document) {
+ const selectors = [
+ "[data-above-composer-conversation-id]",
+ "[data-thread-conversation-id]",
+ "[data-conversation-id]",
+ ];
+ for (const selector of selectors) {
+ const value = elementAttribute(safeQuery(rootNode, selector), selector.slice(1, -1));
+ const key = normalizeConversationKey(value);
+ if (key) return key;
+ }
+ return "";
+ }
+
+ function domToolEventFromElement(element, descriptor, rootNode = document) {
+ if (!element || !descriptor) return null;
+ const name = normalizeToolName(
+ elementAttribute(element, "data-tool-name") ||
+ elementAttribute(element, "data-mcp-tool-name") ||
+ elementAttribute(element, "data-plugin-name") ||
+ descriptor.name
+ );
+ if (!name) return null;
+
+ const testId = normalizeToolName(elementAttribute(element, "data-testid") || descriptor.testId || "tool");
+ const namespace = normalizeToolNamespace(
+ elementAttribute(element, "data-tool-namespace") ||
+ elementAttribute(element, "data-mcp-server") ||
+ elementAttribute(element, "data-plugin-namespace")
+ );
+ const explicitKind = elementAttribute(element, "data-mcp-tool-name") ? "mcp" : descriptor.kind;
+ const conversationKey = currentConversationIdFromDom(rootNode) || "conversation";
+ const turnElement = closestElement(element, "[data-turn-key]");
+ const turnKey = normalizeConversationKey(elementAttribute(turnElement, "data-turn-key"));
+ const scope = turnElement || rootNode;
+ const siblingIndex = Math.max(0, safeQueryAll(scope, descriptor.selector).indexOf(element));
+ const fallbackText = normalizeToolName(
+ element.textContent || element.parentElement?.textContent || element.parentNode?.textContent || ""
+ );
+ const fallbackKey = `${conversationKey}:visible:${testId}:${siblingIndex}:${shortHash(fallbackText)}`;
+ return {
+ kind: classifyToolKind(name, namespace, explicitKind),
+ name,
+ namespace,
+ id: `dom:${turnKey ? `${conversationKey}:${turnKey}:${testId}:${siblingIndex}` : fallbackKey}`,
+ source: "dom-tool-card",
+ timestamp: Date.now(),
+ turnKey,
+ conversationKey,
+ };
+ }
+
+ function extractDomToolCallEvents(rootNode = document) {
+ const events = [];
+ for (const descriptor of DOM_TOOL_DESCRIPTORS) {
+ for (const element of safeQueryAll(rootNode, descriptor.selector)) {
+ const event = domToolEventFromElement(element, descriptor, rootNode);
+ if (event) events.push(event);
+ }
+ }
+ return dedupeToolCallEvents(events);
+ }
+
+ function processDomToolCalls(rootNode = document) {
+ const events = extractDomToolCallEvents(rootNode);
+ if (!events.length) return false;
+ let changed = false;
+ for (const event of events) {
+ changed = recordToolCall(event) || changed;
+ }
+ if (changed) {
+ pruneState();
+ saveState();
+ }
+ return changed;
+ }
+
function processCapturePayload(payload, source, url = "") {
- if (sourceMode === "external") return false;
- processModelPayload(payload);
+ const toolChanged = processToolCallPayload(payload, source);
+ if (sourceMode === "external") return toolChanged;
+ processModelPayload(payload, false);
const candidates = findUsageCandidates(payload);
- if (!candidates.length) return false;
+ if (!candidates.length) return toolChanged;
let changed = false;
for (const candidate of candidates) {
changed = rememberCapturedUsage(candidate, source, url) || changed;
@@ -1349,29 +2146,30 @@
saveState();
render({ animate: true });
}
- return changed;
+ return changed || toolChanged;
}
function shouldProcessStandalonePayload() {
return sourceMode !== "external" && (captureInstalled || !externalSourceAvailable());
}
- function processModelPayload(payload) {
- if (!payload) return false;
+ function processModelPayload(payload, recordTools = true) {
+ const toolChanged = recordTools ? processToolCallPayload(payload, "model") : false;
+ if (!payload) return toolChanged;
if (typeof payload === "string") {
- return parseTextPayloads(payload).some((item) => processModelPayload(item));
+ return parseTextPayloads(payload).some((item) => processModelPayload(item, false)) || toolChanged;
}
if (Array.isArray(payload)) {
- return payload.some((item) => processModelPayload(item));
+ return payload.some((item) => processModelPayload(item, false)) || toolChanged;
}
- if (typeof payload !== "object") return false;
+ if (typeof payload !== "object") return toolChanged;
const conversationKey = extractConversationKey(payload);
let observed = observeAppModelMessage(payload);
observed = observeModel(extractDirectModel(payload), "observed", Date.now(), conversationKey) || observed;
const body = parseMaybeJsonObject(payload.body);
- if (body && body !== payload) observed = processModelPayload(body) || observed;
- return observed;
+ if (body && body !== payload) observed = processModelPayload(body, false) || observed;
+ return observed || toolChanged;
}
function installModelCapture() {
@@ -1955,6 +2753,7 @@
white-space: nowrap;
}
#${PANEL_ID} .codex-daily-models,
+ #${PANEL_ID} .codex-daily-tools,
#${PANEL_ID} .codex-daily-price-panel {
margin-top: 11px;
padding: 10px 11px;
@@ -1977,10 +2776,43 @@
color: var(--color-token-foreground-secondary, #737373);
font-variant-numeric: tabular-nums;
}
- #${PANEL_ID} .codex-daily-model-list {
+ #${PANEL_ID} .codex-daily-model-list,
+ #${PANEL_ID} .codex-daily-tool-list {
display: grid;
gap: 7px;
}
+ #${PANEL_ID} .codex-daily-tool-metrics {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 7px;
+ margin-bottom: 9px;
+ }
+ #${PANEL_ID} .codex-daily-tool-metric {
+ min-width: 0;
+ padding: 7px 8px;
+ border-radius: 9px;
+ background: rgba(127, 127, 127, 0.08);
+ }
+ #${PANEL_ID} .codex-daily-tool-metric span {
+ display: block;
+ margin-bottom: 3px;
+ overflow: hidden;
+ color: var(--color-token-foreground-secondary, #737373);
+ font-size: 10px;
+ line-height: 1.2;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ #${PANEL_ID} .codex-daily-tool-metric strong {
+ display: block;
+ overflow: hidden;
+ color: var(--color-token-foreground, #202020);
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ font-weight: 750;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
#${PANEL_ID} .codex-daily-model-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -2013,6 +2845,39 @@
border-radius: inherit;
background: linear-gradient(90deg, #3BA7FF, #8D6BFF);
}
+ #${PANEL_ID} .codex-daily-tool-row {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ gap: 8px;
+ align-items: center;
+ font-size: 11px;
+ }
+ #${PANEL_ID} .codex-daily-tool-kind {
+ min-width: 34px;
+ padding: 2px 6px;
+ border-radius: 999px;
+ background: rgba(59, 167, 255, 0.12);
+ color: #2676d9;
+ font-size: 10px;
+ font-weight: 700;
+ text-align: center;
+ }
+ #${PANEL_ID} .codex-daily-tool-kind.is-plugin {
+ background: rgba(141, 107, 255, 0.13);
+ color: #7652e5;
+ }
+ #${PANEL_ID} .codex-daily-tool-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: 600;
+ }
+ #${PANEL_ID} .codex-daily-tool-count {
+ color: var(--color-token-foreground, #202020);
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+ }
#${PANEL_ID} .codex-daily-price-panel[hidden] {
display: none;
}
@@ -2242,6 +3107,31 @@
+
Model 价格设置
@@ -2394,6 +3284,65 @@
);
}
+ function renderToolCalls(snapshot) {
+ if (!panel) return;
+ const list = panel.querySelector(".codex-daily-tool-list");
+ const meta = panel.querySelector('[data-field="toolMeta"]');
+ if (meta) {
+ meta.textContent = `调用 ${formatExact(snapshot.toolCallTotal)} · MCP ${formatExact(snapshot.mcpCalls)} · 插件 ${formatExact(snapshot.pluginCalls)}`;
+ }
+ const toolTurnCount = panel.querySelector('[data-field="toolTurnCount"]');
+ if (toolTurnCount) toolTurnCount.textContent = `${formatExact(snapshot.toolTurnCount)} 个`;
+ const toolCallRate = panel.querySelector('[data-field="toolCallRate"]');
+ if (toolCallRate) toolCallRate.textContent = formatPercent(snapshot.toolCallRate);
+ const toolDensity = panel.querySelector('[data-field="toolDensity"]');
+ if (toolDensity) toolDensity.textContent = `${formatDecimal(snapshot.toolsPer100kTokens, 2)} / 10万T`;
+ const toolLinkedUsage = panel.querySelector('[data-field="toolLinkedUsage"]');
+ if (toolLinkedUsage) {
+ const linkedText = snapshot.toolLinkedTurns
+ ? `${formatCompact(snapshot.toolLinkedTokens)} · ${formatCost(snapshot.toolLinkedCost)}`
+ : "待关联";
+ toolLinkedUsage.textContent = linkedText;
+ toolLinkedUsage.title = snapshot.toolLinkedTurns
+ ? `${formatExact(snapshot.toolLinkedTurns)} 个 turn,${formatExact(snapshot.toolLinkedTokens)} Token${snapshot.toolLinkedEstimated ? ",部分为时间估算" : ""}`
+ : "当前采集源没有可精确关联的 turn 用量";
+ }
+ if (!list) return;
+
+ const tools = Array.isArray(snapshot.toolCalls) ? snapshot.toolCalls : [];
+ if (!tools.length) {
+ const row = document.createElement("div");
+ row.className = "codex-daily-tool-row";
+ row.innerHTML = `
+ 无
+ 暂无工具调用
+ 0
+ `;
+ list.replaceChildren(row);
+ return;
+ }
+
+ list.replaceChildren(
+ ...tools.slice(0, 6).map((item) => {
+ const row = document.createElement("div");
+ row.className = "codex-daily-tool-row";
+ const kind = displayToolKind(item.kind);
+ const namespace = normalizeToolNamespace(item.namespace);
+ const name = normalizeToolName(item.name);
+ const displayName = namespace ? `${namespace} / ${name}` : name;
+ const countLabel = item.turns
+ ? `${formatExact(item.count)} / ${formatExact(item.turns)}`
+ : formatExact(item.count);
+ row.innerHTML = `
+ ${kind}
+ ${escapeHtml(displayName)}
+ ${countLabel}
+ `;
+ return row;
+ })
+ );
+ }
+
function renderPriceSettings(snapshot) {
if (!panel) return;
const list = panel.querySelector(".codex-daily-price-list");
@@ -2575,6 +3524,7 @@
return {
rect,
gap: Math.max(FLOATING_SAFE_GAP, Number(anchor?.gap) || 0),
+ mode: anchor?.mode === "hard" ? "hard" : "soft",
};
}
@@ -2610,9 +3560,13 @@
const layoutTop = clampTop(anchor.rect.top + (anchor.rect.height - safeHeight) / 2);
const layoutRight = viewportWidth - anchor.rect.left + anchor.gap;
const candidate = candidateRectFromRight(layoutRight, layoutTop, safeWidth, safeHeight, viewportWidth);
- if (candidateIsClear(candidate)) {
+ if (candidateFits(candidate) && (anchor.mode === "hard" || candidateIsClear(candidate))) {
return layoutFromLeft(candidate.left, safeWidth, false, layoutTop);
}
+ const compactCandidate = candidateRectFromRight(layoutRight, layoutTop, compactWidth, safeHeight, viewportWidth);
+ if (candidateFits(compactCandidate) && (anchor.mode === "hard" || candidateIsClear(compactCandidate))) {
+ return layoutFromLeft(compactCandidate.left, compactWidth, true, layoutTop);
+ }
}
const defaultRight = clampRight(FLOATING_DEFAULT_RIGHT);
@@ -2725,9 +3679,9 @@
const anchors = [];
const plusMenu = document.getElementById(CODEX_PLUS_MENU_ID);
const plusMenuRect = visibleTopRect(plusMenu);
- if (plusMenuRect) anchors.push({ rect: plusMenuRect, gap: FLOATING_SAFE_GAP });
+ if (plusMenuRect) anchors.push({ rect: plusMenuRect, gap: FLOATING_SAFE_GAP, mode: "hard" });
const headerAnchor = findHeaderToolbarAnchor();
- if (headerAnchor) anchors.push(headerAnchor);
+ if (headerAnchor) anchors.push({ ...headerAnchor, mode: "hard" });
return anchors;
}
@@ -2946,6 +3900,7 @@
panel.querySelector(".codex-daily-status-text").textContent = sourceStatusText(snapshot);
renderPanelTrend(buildTrendData(selectedDateKey));
renderModelBreakdown(snapshot);
+ renderToolCalls(snapshot);
if (panel.querySelector(".codex-daily-price-panel")?.hidden === false) {
renderPriceSettings(snapshot);
}
@@ -2985,7 +3940,9 @@
saveState();
}
- const changed = syncFromSource();
+ const sourceChanged = syncFromSource();
+ const domToolChanged = processDomToolCalls();
+ const changed = sourceChanged || domToolChanged;
mountRoot();
render({ animate: changed });
return aggregateDay();
@@ -3041,6 +3998,12 @@
extractDirectModel,
extractModelFromAppMessage,
observeAppModelMessage,
+ normalizeToolName,
+ extractToolCallEvents,
+ processToolCallPayload,
+ recordToolCall,
+ extractDomToolCallEvents,
+ processDomToolCalls,
calculateUsageCost,
formatCost,
getDateKey,
@@ -3087,7 +4050,10 @@
mountRoot();
refresh();
- observer = new MutationObserver(() => mountRoot());
+ observer = new MutationObserver(() => {
+ mountRoot();
+ if (processDomToolCalls()) render({ animate: false });
+ });
observer.observe(document.documentElement, { childList: true, subtree: true });
document.addEventListener("pointerdown", handleDocumentPointerDown, true);
window.addEventListener("resize", positionPanel);
From ba52416d6f959b5b5f27a00b0f154a62b33fffa3 Mon Sep 17 00:00:00 2001
From: "K.T.S" <38993658@qq.com>
Date: Thu, 9 Jul 2026 15:48:51 +0800
Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E6=AF=8F=E6=97=A5?=
=?UTF-8?q?=E7=94=A8=E9=87=8F=E5=B8=83=E5=B1=80=E4=B8=8E=E5=B7=A5=E5=85=B7?=
=?UTF-8?q?=E5=85=B3=E8=81=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.json | 4 +-
scripts/codex-daily-token-usage.js | 257 +++++++++++++++++++++++------
2 files changed, 209 insertions(+), 52 deletions(-)
diff --git a/index.json b/index.json
index a105795..855541c 100644
--- a/index.json
+++ b/index.json
@@ -55,7 +55,7 @@
"id": "codex-daily-token-usage",
"name": "Codex Daily Token Usage",
"description": "每日 Token 统计,近 5 日滚动存储,自动复用已有采集,支持 Model 价格、成本估算、趋势明细和分享图",
- "version": "1.4.7",
+ "version": "1.4.9",
"author": "kts-kris",
"tags": [
"codex",
@@ -67,7 +67,7 @@
],
"homepage": "",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-daily-token-usage.js",
- "sha256": "daa394c603bc67ff5a3e52930bad7f41d7cf3e4a750883aedc2e7f3397402814"
+ "sha256": "93a01df1cc17d7e715aa08aa8720b8c4a4cde3a37f6abdd3f6c445e890a1ce9d"
},
{
"id": "codex-list-pagebuster",
diff --git a/scripts/codex-daily-token-usage.js b/scripts/codex-daily-token-usage.js
index 31e7f22..0db757a 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.7
+// @version 1.4.9
// @description 每日 Token 统计,近 5 日滚动存储,优先复用已有采集,必要时内置采集,支持 Model 价格、成本估算、日期切换、5 日趋势与分享图。
// @match app://-/*
// @run-at document-start
@@ -10,7 +10,7 @@
(() => {
"use strict";
- const VERSION = "1.4.7";
+ const VERSION = "1.4.9";
const API_KEY = "__codexDailyTokenUsage";
const SOURCE_API_KEY = "__codexTokenUsage";
const STORAGE_KEY = "__codexDailyTokenUsageV1";
@@ -20,8 +20,10 @@
const STYLE_ID = "codex-daily-token-usage-style";
const CODEX_PLUS_MENU_ID = "codex-plus-menu";
const APP_HEADER_SELECTOR = ".app-header-tint";
+ const APP_HEADER_SURFACE_SELECTOR = '[data-testid="app-shell-header-context-menu-surface"]';
const HEADER_TOOLBAR_CLUSTER_SELECTOR = ".ms-auto.flex.shrink-0.items-center";
const HEADER_TOOLBAR_CLASS_SELECTOR = '[class*="ms-auto"][class*="shrink-0"][class*="items-center"]';
+ const TOP_OBSTACLE_SELECTOR = `button,[role='button'],input,select,textarea,a[href],#${CODEX_PLUS_MENU_ID},[data-testid]`;
const POLL_INTERVAL_MS = 1000;
const RETAIN_DAYS = 5;
const MAX_TURNS_PER_DAY = 2000;
@@ -64,6 +66,8 @@
let panel = null;
let style = null;
let observer = null;
+ let resizeObserver = null;
+ let layoutRaf = 0;
let pollTimer = null;
let midnightTimer = null;
let closeTimer = null;
@@ -86,6 +90,7 @@
let lastObservedModelAt = 0;
let lastObservedModelConfidence = "unknown";
const modelByConversationKey = new Map();
+ const resizeObservedNodes = new WeakSet();
function toCount(value) {
const number = Number(value);
@@ -164,6 +169,13 @@
return null;
}
+ function parseUuidV7Timestamp(value) {
+ const text = String(value || "").trim();
+ const match = /\b([0-9a-f]{8})-?([0-9a-f]{4})-?7[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}\b/i.exec(text);
+ if (!match) return null;
+ return parseTimestamp(Number.parseInt(`${match[1]}${match[2]}`, 16));
+ }
+
function latestNestedTimestamp(items) {
if (!Array.isArray(items)) return null;
let latest = null;
@@ -709,6 +721,7 @@
const dateKey = getDateKey(timestamp);
const usage = normalizeUsage(turn.usage);
const modelMeta = extractTurnModel(turn, timestamp);
+ const conversationKey = normalizeConversationKey(turn.conversationKey || turn.conversationId || turn.conversation_id || extractConversationKey(turn));
const day = state.days[dateKey] || { turns: {}, updatedAt: 0 };
const existing = day.turns[turn.turnId];
const candidate = {
@@ -722,14 +735,25 @@
source: String(turn.source || "turn-aggregate"),
model: modelMeta.model,
modelConfidence: modelMeta.confidence,
+ conversationKey,
};
if (existing && existing.total > candidate.total) {
- if (candidate.model !== UNKNOWN_MODEL && (!existing.model || existing.model === UNKNOWN_MODEL)) {
+ if (
+ (candidate.model !== UNKNOWN_MODEL && (!existing.model || existing.model === UNKNOWN_MODEL)) ||
+ (candidate.conversationKey && !existing.conversationKey)
+ ) {
day.turns[turn.turnId] = {
...existing,
- model: candidate.model,
- modelConfidence: candidate.modelConfidence,
+ 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,
+ conversationKey: existing.conversationKey || candidate.conversationKey,
updatedAt: Math.max(existing.updatedAt || 0, candidate.updatedAt),
};
state.days[dateKey] = day;
@@ -756,6 +780,7 @@
candidate.model !== UNKNOWN_MODEL || !existing.model || existing.model === UNKNOWN_MODEL
? candidate.modelConfidence
: existing.modelConfidence || "unknown",
+ conversationKey: candidate.conversationKey || existing.conversationKey || "",
}
: candidate;
@@ -868,6 +893,25 @@
.filter(Boolean);
}
+ function turnConversationKeys(turn) {
+ return conversationKeyVariants(turn?.conversationKey).concat(conversationKeyVariants(turn?.conversationId));
+ }
+
+ function eventConversationMatchesTurn(event, turn) {
+ const eventKeys = conversationKeyVariants(event?.conversationKey);
+ if (!eventKeys.length) return true;
+ const turnKeys = new Set(turnConversationKeys(turn));
+ if (!turnKeys.size) return true;
+ return eventKeys.some((key) => turnKeys.has(key));
+ }
+
+ function toolEventUsageTimestamp(event) {
+ if (event?.source === "dom-tool-card") {
+ return parseUuidV7Timestamp(event.turnKey) || parseUuidV7Timestamp(event.id);
+ }
+ return parseTimestamp(event?.timestamp);
+ }
+
function findExactToolTurnId(event, turnEntries) {
const keys = conversationKeyVariants(event.turnKey);
if (!keys.length) return "";
@@ -879,10 +923,11 @@
}
function findNearestToolTurnId(event, turnEntries) {
- const timestamp = parseTimestamp(event.timestamp);
- if (!timestamp || event.source === "dom-tool-card") return "";
+ const timestamp = toolEventUsageTimestamp(event);
+ if (!timestamp) return "";
let best = null;
for (const [id, turn] of turnEntries) {
+ if (!eventConversationMatchesTurn(event, turn)) continue;
const updatedAt = toCount(turn?.updatedAt);
if (!updatedAt) continue;
const distance = Math.abs(updatedAt - timestamp);
@@ -3545,6 +3590,7 @@
width,
compact,
});
+ const layoutFromCandidate = (candidate, compact = false) => layoutFromLeft(candidate.left, candidate.width, compact, candidate.top);
const topObstacles = obstacleRects
.map(normalizeRect)
.filter((rect) => rect && rect.bottom > 0 && rect.top < FLOATING_SCAN_TOP && rect.right > 0 && rect.left < viewportWidth);
@@ -3555,63 +3601,109 @@
candidate.bottom <= safeViewportHeight;
const candidateIsClear = (candidate) =>
candidateFits(candidate) && !topObstacles.some((rect) => rectsOverlap(candidate, rect, FLOATING_SAFE_GAP));
+ const collectRowGaps = (layoutTop, rightLimit = viewportWidth - PANEL_MARGIN) => {
+ const safeRightLimit = Math.min(Math.max(PANEL_MARGIN, rightLimit), viewportWidth - PANEL_MARGIN);
+ if (safeRightLimit <= PANEL_MARGIN) return [];
+
+ const scanRect = {
+ left: PANEL_MARGIN,
+ right: safeRightLimit,
+ top: layoutTop,
+ bottom: layoutTop + safeHeight,
+ width: safeRightLimit - PANEL_MARGIN,
+ height: safeHeight,
+ };
+ const blocked = topObstacles
+ .filter((rect) => rectsOverlap(scanRect, rect, FLOATING_SAFE_GAP))
+ .map((rect) => ({
+ left: Math.max(PANEL_MARGIN, rect.left - FLOATING_SAFE_GAP),
+ right: Math.min(safeRightLimit, rect.right + FLOATING_SAFE_GAP),
+ }))
+ .filter((rect) => rect.right > rect.left)
+ .sort((a, b) => a.left - b.left);
+
+ let cursor = PANEL_MARGIN;
+ const gaps = [];
+ for (const rect of blocked) {
+ if (rect.left > cursor) gaps.push({ left: cursor, right: rect.left });
+ cursor = Math.max(cursor, rect.right);
+ }
+ if (cursor < safeRightLimit) gaps.push({ left: cursor, right: safeRightLimit });
+ return gaps;
+ };
+ const findRightmostClearLayout = (layoutTop, rightLimit = viewportWidth - PANEL_MARGIN) => {
+ const gaps = collectRowGaps(layoutTop, rightLimit).sort((a, b) => b.right - a.right);
+ const pick = (candidateWidth, compact) => {
+ for (const gap of gaps) {
+ if (gap.right - gap.left < candidateWidth) continue;
+ const candidate = {
+ left: gap.right - candidateWidth,
+ right: gap.right,
+ top: layoutTop,
+ bottom: layoutTop + safeHeight,
+ width: candidateWidth,
+ height: safeHeight,
+ };
+ if (candidateIsClear(candidate)) return layoutFromCandidate(candidate, compact);
+ }
+ return null;
+ };
+ return pick(safeWidth, false) || pick(compactWidth, true);
+ };
for (const anchor of preferredAnchors.map(normalizeFloatingAnchor).filter(Boolean)) {
const layoutTop = clampTop(anchor.rect.top + (anchor.rect.height - safeHeight) / 2);
- const layoutRight = viewportWidth - anchor.rect.left + anchor.gap;
+ const anchorRightLimit = Math.min(viewportWidth - PANEL_MARGIN, anchor.rect.left - anchor.gap);
+ const layoutRight = viewportWidth - anchorRightLimit;
const candidate = candidateRectFromRight(layoutRight, layoutTop, safeWidth, safeHeight, viewportWidth);
- if (candidateFits(candidate) && (anchor.mode === "hard" || candidateIsClear(candidate))) {
- return layoutFromLeft(candidate.left, safeWidth, false, layoutTop);
+ if (candidateIsClear(candidate)) {
+ return layoutFromCandidate(candidate, false);
}
const compactCandidate = candidateRectFromRight(layoutRight, layoutTop, compactWidth, safeHeight, viewportWidth);
- if (candidateFits(compactCandidate) && (anchor.mode === "hard" || candidateIsClear(compactCandidate))) {
- return layoutFromLeft(compactCandidate.left, compactWidth, true, layoutTop);
+ if (candidateIsClear(compactCandidate)) {
+ return layoutFromCandidate(compactCandidate, true);
}
+ const anchoredGapLayout = findRightmostClearLayout(layoutTop, anchorRightLimit);
+ if (anchoredGapLayout) return anchoredGapLayout;
}
const defaultRight = clampRight(FLOATING_DEFAULT_RIGHT);
const defaultRect = candidateRectFromRight(defaultRight, top, safeWidth, safeHeight, viewportWidth);
- if (!topObstacles.some((rect) => rectsOverlap(defaultRect, rect, FLOATING_SAFE_GAP))) {
+ if (candidateIsClear(defaultRect)) {
return { top, right: defaultRight, left: defaultRect.left, width: safeWidth, compact: false };
}
- const blocked = topObstacles
- .map((rect) => ({
- left: Math.max(PANEL_MARGIN, rect.left - FLOATING_SAFE_GAP),
- right: Math.min(viewportWidth - PANEL_MARGIN, rect.right + FLOATING_SAFE_GAP),
- }))
- .sort((a, b) => a.left - b.left);
- let cursor = PANEL_MARGIN;
- const gaps = [];
- for (const rect of blocked) {
- if (rect.left > cursor) gaps.push({ left: cursor, right: rect.left });
- cursor = Math.max(cursor, rect.right);
- }
- if (cursor < viewportWidth - PANEL_MARGIN) gaps.push({ left: cursor, right: viewportWidth - PANEL_MARGIN });
-
- const fullGap = gaps
- .filter((gap) => gap.right - gap.left >= safeWidth)
- .sort((a, b) => b.right - a.right)[0];
- if (fullGap) {
- return layoutFromLeft(fullGap.right - safeWidth, safeWidth, false);
- }
-
- const compactGap = gaps
- .filter((gap) => gap.right - gap.left >= compactWidth)
- .sort((a, b) => b.right - a.right)[0];
- if (compactGap) {
- return layoutFromLeft(compactGap.right - compactWidth, compactWidth, true);
- }
+ const clearGapLayout = findRightmostClearLayout(top);
+ if (clearGapLayout) return clearGapLayout;
const fallbackRight = Math.min(Math.max(PANEL_MARGIN, defaultRight), Math.max(PANEL_MARGIN, viewportWidth - PANEL_MARGIN - compactWidth));
const fallbackRect = candidateRectFromRight(fallbackRight, top, compactWidth, safeHeight, viewportWidth);
return { top, right: fallbackRight, left: fallbackRect.left, width: compactWidth, compact: true };
}
- function collectTopObstacleRects() {
+ function isPrimaryTopObstacleNode(node) {
+ return !!node?.matches?.(`button,[role='button'],input,select,textarea,a[href],#${CODEX_PLUS_MENU_ID}`);
+ }
+
+ function findAppHeaderElement() {
+ return (
+ document.querySelector(APP_HEADER_SELECTOR) ||
+ document.querySelector(APP_HEADER_SURFACE_SELECTOR) ||
+ document.querySelector("header")
+ );
+ }
+
+ function isTopChromeObstacleNode(node, style) {
+ if (!node) return false;
+ if (node.id === CODEX_PLUS_MENU_ID || node.closest?.(`#${CODEX_PLUS_MENU_ID}`)) return true;
+ const header = findAppHeaderElement();
+ if (header?.contains?.(node)) return true;
+ return style?.position === "fixed" || style?.position === "sticky";
+ }
+
+ function collectTopObstacleEntries() {
if (!document.body?.querySelectorAll) return [];
- const selector = `button,[role='button'],input,select,textarea,#${CODEX_PLUS_MENU_ID},[data-testid]`;
- return Array.from(document.body.querySelectorAll(selector))
+ const entries = Array.from(document.body.querySelectorAll(TOP_OBSTACLE_SELECTOR))
.filter((node) => !root?.contains(node) && !panel?.contains(node))
.map((node) => {
const rect = normalizeRect(node.getBoundingClientRect?.());
@@ -3620,9 +3712,61 @@
if (rect.width > innerWidth * 0.85 || rect.height > FLOATING_SCAN_TOP) return null;
const style = typeof getComputedStyle === "function" ? getComputedStyle(node) : null;
if (style && (style.display === "none" || style.visibility === "hidden" || style.opacity === "0")) return null;
- return rect;
+ if (!isTopChromeObstacleNode(node, style)) return null;
+ return { node, rect };
})
.filter(Boolean);
+
+ return entries.filter((entry, index) => {
+ if (isPrimaryTopObstacleNode(entry.node)) return true;
+ return !entries.some((other, otherIndex) => {
+ if (otherIndex === index || !entry.node.contains?.(other.node)) return false;
+ if (!rectsOverlap(entry.rect, other.rect)) return false;
+ return other.rect.width <= entry.rect.width && other.rect.height <= entry.rect.height;
+ });
+ });
+ }
+
+ function collectTopObstacleRects() {
+ return collectTopObstacleEntries().map((entry) => entry.rect);
+ }
+
+ function scheduleMountRoot() {
+ if (destroyed || layoutRaf) return;
+ const run = () => {
+ layoutRaf = 0;
+ mountRoot();
+ };
+ layoutRaf = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(run) : window.setTimeout(run, 16);
+ }
+
+ function observeLayoutNode(node) {
+ if (!node || resizeObservedNodes.has(node)) return;
+ if (root?.contains(node) || panel?.contains(node)) return;
+ if (typeof ResizeObserver !== "function") return;
+ if (!resizeObserver) {
+ resizeObserver = new ResizeObserver(() => {
+ scheduleMountRoot();
+ });
+ }
+ try {
+ resizeObserver.observe(node);
+ resizeObservedNodes.add(node);
+ } catch {
+ // 个别节点不可观察时忽略,MutationObserver 仍会兜底。
+ }
+ }
+
+ function updateLayoutResizeObservers(obstacleEntries = []) {
+ observeLayoutNode(document.body);
+ observeLayoutNode(findAppHeaderElement());
+ observeLayoutNode(document.getElementById(CODEX_PLUS_MENU_ID));
+ for (const entry of obstacleEntries) observeLayoutNode(entry.node);
+ }
+
+ function handleWindowResize() {
+ scheduleMountRoot();
+ positionPanel();
}
function numericCssValue(value) {
@@ -3660,7 +3804,7 @@
}
function findHeaderToolbarAnchor() {
- const header = document.querySelector(APP_HEADER_SELECTOR) || document.querySelector("header");
+ const header = findAppHeaderElement();
if (!header) return null;
const title = headerTitleRegion(header);
const toolbarButtons = Array.from(header.querySelectorAll("button"))
@@ -3734,12 +3878,14 @@
}
root.dataset.layout = "top-toolbar";
const rect = root.getBoundingClientRect();
+ const obstacleEntries = collectTopObstacleEntries();
+ updateLayoutResizeObservers(obstacleEntries);
const layout = resolveFloatingLayout(
rect.width || FLOATING_MIN_WIDTH,
rect.height || FLOATING_HEIGHT,
innerWidth,
innerHeight,
- collectTopObstacleRects(),
+ obstacleEntries.map((entry) => entry.rect),
collectFloatingAnchors()
);
root.style.top = `${Math.round(layout.top)}px`;
@@ -3961,10 +4107,16 @@
if (midnightTimer) window.clearTimeout(midnightTimer);
if (closeTimer) window.clearTimeout(closeTimer);
if (shareFeedbackTimer) window.clearTimeout(shareFeedbackTimer);
+ if (layoutRaf) {
+ if (typeof window.cancelAnimationFrame === "function") window.cancelAnimationFrame(layoutRaf);
+ else window.clearTimeout(layoutRaf);
+ layoutRaf = 0;
+ }
observer?.disconnect();
+ resizeObserver?.disconnect();
restoreStandaloneCapture();
document.removeEventListener("pointerdown", handleDocumentPointerDown, true);
- window.removeEventListener("resize", positionPanel);
+ window.removeEventListener("resize", handleWindowResize);
root?.remove();
panel?.remove();
style?.remove();
@@ -4051,12 +4203,17 @@
refresh();
observer = new MutationObserver(() => {
- mountRoot();
+ scheduleMountRoot();
if (processDomToolCalls()) render({ animate: false });
});
- observer.observe(document.documentElement, { childList: true, subtree: true });
+ observer.observe(document.documentElement, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: ["aria-hidden", "class", "data-state", "hidden", "style"],
+ });
document.addEventListener("pointerdown", handleDocumentPointerDown, true);
- window.addEventListener("resize", positionPanel);
+ window.addEventListener("resize", handleWindowResize);
pollTimer = window.setInterval(refresh, POLL_INTERVAL_MS);
scheduleMidnightRefresh();
}
From 033bf22f7d369053b482197ff0d6fd601436d478 Mon Sep 17 00:00:00 2001
From: "K.T.S" <38993658@qq.com>
Date: Thu, 9 Jul 2026 20:39:01 +0800
Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=87=8D=E5=90=AF?=
=?UTF-8?q?=E5=90=8E=E6=AF=8F=E6=97=A5=E7=94=A8=E9=87=8F=E6=A0=87=E9=A2=98?=
=?UTF-8?q?=E6=A0=8F=E9=94=99=E4=BD=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.json | 4 ++--
scripts/codex-daily-token-usage.js | 8 +++++---
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/index.json b/index.json
index 855541c..6277b0e 100644
--- a/index.json
+++ b/index.json
@@ -55,7 +55,7 @@
"id": "codex-daily-token-usage",
"name": "Codex Daily Token Usage",
"description": "每日 Token 统计,近 5 日滚动存储,自动复用已有采集,支持 Model 价格、成本估算、趋势明细和分享图",
- "version": "1.4.9",
+ "version": "1.4.11",
"author": "kts-kris",
"tags": [
"codex",
@@ -67,7 +67,7 @@
],
"homepage": "",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-daily-token-usage.js",
- "sha256": "93a01df1cc17d7e715aa08aa8720b8c4a4cde3a37f6abdd3f6c445e890a1ce9d"
+ "sha256": "005762071687a3720160812785b5811eb7bb4652aea9b783cf8cb3bddf7c2def"
},
{
"id": "codex-list-pagebuster",
diff --git a/scripts/codex-daily-token-usage.js b/scripts/codex-daily-token-usage.js
index 0db757a..d79c9b2 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.9
+// @version 1.4.11
// @description 每日 Token 统计,近 5 日滚动存储,优先复用已有采集,必要时内置采集,支持 Model 价格、成本估算、日期切换、5 日趋势与分享图。
// @match app://-/*
// @run-at document-start
@@ -10,7 +10,7 @@
(() => {
"use strict";
- const VERSION = "1.4.9";
+ const VERSION = "1.4.11";
const API_KEY = "__codexDailyTokenUsage";
const SOURCE_API_KEY = "__codexTokenUsage";
const STORAGE_KEY = "__codexDailyTokenUsageV1";
@@ -3696,6 +3696,8 @@
function isTopChromeObstacleNode(node, style) {
if (!node) return false;
if (node.id === CODEX_PLUS_MENU_ID || node.closest?.(`#${CODEX_PLUS_MENU_ID}`)) return true;
+ if (node.matches?.(APP_HEADER_SURFACE_SELECTOR)) return false;
+ if (!isPrimaryTopObstacleNode(node) && style?.pointerEvents === "none") return false;
const header = findAppHeaderElement();
if (header?.contains?.(node)) return true;
return style?.position === "fixed" || style?.position === "sticky";
@@ -3889,7 +3891,7 @@
collectFloatingAnchors()
);
root.style.top = `${Math.round(layout.top)}px`;
- root.style.right = `${Math.round(layout.right)}px`;
+ root.style.right = `${Math.ceil(layout.right)}px`;
root.style.left = "auto";
root.style.bottom = "auto";
root.style.transform = "none";
From ef9d0e0d6a8bb88a46625d80aa4aee715ab8e4b8 Mon Sep 17 00:00:00 2001
From: "K.T.S" <38993658@qq.com>
Date: Fri, 10 Jul 2026 14:48:22 +0800
Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=E9=A2=84=E7=BD=AE=20OpenAI=20?=
=?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=8F=82=E8=80=83=E4=BB=B7=E6=A0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
index.json | 6 +-
scripts/codex-daily-token-usage.js | 176 ++++++++++++++++++++++++++---
2 files changed, 161 insertions(+), 21 deletions(-)
diff --git a/index.json b/index.json
index 6277b0e..de45d58 100644
--- a/index.json
+++ b/index.json
@@ -1,6 +1,6 @@
{
"version": 1,
- "updated_at": "2026-07-07T07:45:09.000Z",
+ "updated_at": "2026-07-10T06:46:05.000Z",
"scripts": [
{
"id": "codex-context-used-meter",
@@ -55,7 +55,7 @@
"id": "codex-daily-token-usage",
"name": "Codex Daily Token Usage",
"description": "每日 Token 统计,近 5 日滚动存储,自动复用已有采集,支持 Model 价格、成本估算、趋势明细和分享图",
- "version": "1.4.11",
+ "version": "1.4.12",
"author": "kts-kris",
"tags": [
"codex",
@@ -67,7 +67,7 @@
],
"homepage": "",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-daily-token-usage.js",
- "sha256": "005762071687a3720160812785b5811eb7bb4652aea9b783cf8cb3bddf7c2def"
+ "sha256": "66c6af995d1908961b0931d0a41dd804d807b041a4291eb86144627becca4dbe"
},
{
"id": "codex-list-pagebuster",
diff --git a/scripts/codex-daily-token-usage.js b/scripts/codex-daily-token-usage.js
index d79c9b2..5b8a9f1 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.11
+// @version 1.4.12
// @description 每日 Token 统计,近 5 日滚动存储,优先复用已有采集,必要时内置采集,支持 Model 价格、成本估算、日期切换、5 日趋势与分享图。
// @match app://-/*
// @run-at document-start
@@ -10,7 +10,7 @@
(() => {
"use strict";
- const VERSION = "1.4.11";
+ const VERSION = "1.4.12";
const API_KEY = "__codexDailyTokenUsage";
const SOURCE_API_KEY = "__codexTokenUsage";
const STORAGE_KEY = "__codexDailyTokenUsageV1";
@@ -39,6 +39,74 @@
const MODEL_BIND_WINDOW_MS = 30 * 60 * 1000;
const UNKNOWN_MODEL = "Unknown";
const PRICE_FIELDS = ["input", "cachedInput", "output", "reasoning"];
+ const DEFAULT_OPENAI_PRICE_SOURCE = "OpenAI API Pricing · Standard · USD / 1M tokens";
+ const DEFAULT_OPENAI_MODEL_PRICES = Object.freeze({
+ "gpt-5.6-sol": { input: 5, cachedInput: 0.5, output: 30 },
+ "gpt-5.6-terra": { input: 2.5, cachedInput: 0.25, output: 15 },
+ "gpt-5.6-luna": { input: 1, cachedInput: 0.1, output: 6 },
+ "gpt-5.5": { input: 5, cachedInput: 0.5, output: 30 },
+ "gpt-5.5-pro": { input: 30, output: 180 },
+ "gpt-5.4": { input: 2.5, cachedInput: 0.25, output: 15 },
+ "gpt-5.4-mini": { input: 0.75, cachedInput: 0.075, output: 4.5 },
+ "gpt-5.4-nano": { input: 0.2, cachedInput: 0.02, output: 1.25 },
+ "gpt-5.4-pro": { input: 30, output: 180 },
+ "gpt-5.3-chat-latest": { input: 1.75, cachedInput: 0.175, output: 14 },
+ "gpt-5.2": { input: 1.75, cachedInput: 0.175, output: 14 },
+ "gpt-5.2-pro": { input: 21, output: 168 },
+ "gpt-5.2-chat-latest": { input: 1.75, cachedInput: 0.175, output: 14 },
+ "gpt-5.1": { input: 1.25, cachedInput: 0.125, output: 10 },
+ "gpt-5.1-chat-latest": { input: 1.25, cachedInput: 0.125, output: 10 },
+ "gpt-5": { input: 1.25, cachedInput: 0.125, output: 10 },
+ "gpt-5-chat-latest": { input: 1.25, cachedInput: 0.125, output: 10 },
+ "gpt-5-mini": { input: 0.25, cachedInput: 0.025, output: 2 },
+ "gpt-5-nano": { input: 0.05, cachedInput: 0.005, output: 0.4 },
+ "gpt-5-pro": { input: 15, output: 120 },
+ "gpt-5.3-codex": { input: 1.75, cachedInput: 0.175, output: 14 },
+ "gpt-5.2-codex": { input: 1.75, cachedInput: 0.175, output: 14 },
+ "gpt-5.1-codex-max": { input: 1.25, cachedInput: 0.125, output: 10 },
+ "gpt-5.1-codex": { input: 1.25, cachedInput: 0.125, output: 10 },
+ "gpt-5-codex": { input: 1.25, cachedInput: 0.125, output: 10 },
+ "gpt-5.1-codex-mini": { input: 0.25, cachedInput: 0.025, output: 2 },
+ "codex-mini-latest": { input: 1.5, cachedInput: 0.375, output: 6 },
+ "chat-latest": { input: 5, cachedInput: 0.5, output: 30 },
+ "chatgpt-4o-latest": { input: 5, output: 15 },
+ "gpt-4.1": { input: 2, cachedInput: 0.5, output: 8 },
+ "gpt-4.1-mini": { input: 0.4, cachedInput: 0.1, output: 1.6 },
+ "gpt-4.1-nano": { input: 0.1, cachedInput: 0.025, output: 0.4 },
+ "gpt-4o": { input: 2.5, cachedInput: 1.25, output: 10 },
+ "gpt-4o-2024-05-13": { input: 5, output: 15 },
+ "gpt-4o-mini": { input: 0.15, cachedInput: 0.075, output: 0.6 },
+ o1: { input: 15, cachedInput: 7.5, output: 60 },
+ "o1-pro": { input: 150, output: 600 },
+ "o3-pro": { input: 20, output: 80 },
+ o3: { input: 2, cachedInput: 0.5, output: 8 },
+ "o4-mini": { input: 1.1, cachedInput: 0.275, output: 4.4 },
+ "o3-mini": { input: 1.1, cachedInput: 0.55, output: 4.4 },
+ "o1-mini": { input: 1.1, cachedInput: 0.55, output: 4.4 },
+ "gpt-4-turbo-2024-04-09": { input: 10, output: 30 },
+ "gpt-4-0125-preview": { input: 10, output: 30 },
+ "gpt-4-1106-preview": { input: 10, output: 30 },
+ "gpt-4-1106-vision-preview": { input: 10, output: 30 },
+ "gpt-4-0613": { input: 30, output: 60 },
+ "gpt-4-0314": { input: 30, output: 60 },
+ "gpt-4-32k": { input: 60, output: 120 },
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
+ "gpt-3.5-turbo-0125": { input: 0.5, output: 1.5 },
+ "gpt-3.5-turbo-1106": { input: 1, output: 2 },
+ "gpt-3.5-turbo-0613": { input: 1.5, output: 2 },
+ "gpt-3.5-0301": { input: 1.5, output: 2 },
+ "gpt-3.5-turbo-instruct": { input: 1.5, output: 2 },
+ "gpt-3.5-turbo-16k-0613": { input: 3, output: 4 },
+ "davinci-002": { input: 2, output: 2 },
+ "babbage-002": { input: 0.4, output: 0.4 },
+ "gpt-5.5-cyber": { input: 12.5, cachedInput: 1.25, output: 75 },
+ "gpt-5-search-api": { input: 1.25, cachedInput: 0.125, output: 10 },
+ "gpt-4o-search-preview": { input: 2.5, output: 10 },
+ "gpt-4o-mini-search-preview": { input: 0.15, output: 0.6 },
+ "o3-deep-research": { input: 10, cachedInput: 2.5, output: 40 },
+ "o4-mini-deep-research": { input: 2, cachedInput: 0.5, output: 8 },
+ "computer-use-preview": { input: 3, output: 12 },
+ });
const FLOATING_TOP = 2;
const FLOATING_DEFAULT_RIGHT = 280;
const FLOATING_SAFE_GAP = 8;
@@ -587,6 +655,24 @@
return PRICE_FIELDS.every((field) => normalized[field] == null);
}
+ function defaultModelPrice(model) {
+ const normalized = normalizeModelName(model);
+ const lookup = normalized.replace(/\s+\([^)]*\)\s*$/, "").toLowerCase();
+ const entry = normalized
+ ? DEFAULT_OPENAI_MODEL_PRICES[normalized] || DEFAULT_OPENAI_MODEL_PRICES[lookup]
+ : null;
+ return entry ? normalizePriceEntry(entry) : null;
+ }
+
+ function mergePriceEntries(base, override) {
+ const merged = normalizePriceEntry(base);
+ const custom = normalizePriceEntry(override);
+ for (const field of PRICE_FIELDS) {
+ if (custom[field] != null) merged[field] = custom[field];
+ }
+ return isPriceEntryEmpty(merged) ? null : merged;
+ }
+
function loadPriceConfig() {
try {
const parsed = JSON.parse(localStorage.getItem(PRICE_STORAGE_KEY) || "null");
@@ -615,9 +701,24 @@
}
}
- function getModelPrice(model) {
+ function getModelPriceInfo(model) {
const normalized = normalizeModelName(model);
- return normalized ? priceConfig.models[normalized] || null : null;
+ if (!normalized) return { price: null, source: "none", hasCustom: false, hasDefault: false };
+ const custom = normalizePriceEntry(priceConfig.models[normalized]);
+ const defaultPrice = defaultModelPrice(normalized);
+ const hasCustom = !isPriceEntryEmpty(custom);
+ const hasDefault = !isPriceEntryEmpty(defaultPrice);
+ const price = mergePriceEntries(defaultPrice, custom);
+ return {
+ price,
+ source: hasCustom ? "custom" : hasDefault ? "default" : "none",
+ hasCustom,
+ hasDefault,
+ };
+ }
+
+ function getModelPrice(model) {
+ return getModelPriceInfo(model).price;
}
function setModelPrice(model, entry) {
@@ -996,6 +1097,8 @@
updatedAt: 0,
cost: 0,
pricedModels: 0,
+ customPricedModels: 0,
+ defaultPricedModels: 0,
modelsByName: {},
models: [],
toolCallsByKey: {},
@@ -1016,13 +1119,19 @@
);
summary.models = Object.values(summary.modelsByName)
.map((modelSummary) => {
- const costInfo = calculateUsageCost(modelSummary, getModelPrice(modelSummary.model));
+ const priceInfo = getModelPriceInfo(modelSummary.model);
+ const costInfo = calculateUsageCost(modelSummary, priceInfo.price);
summary.cost += costInfo.cost;
- if (costInfo.configured) summary.pricedModels += 1;
+ if (costInfo.configured) {
+ summary.pricedModels += 1;
+ if (priceInfo.source === "custom") summary.customPricedModels += 1;
+ else if (priceInfo.source === "default") summary.defaultPricedModels += 1;
+ }
return {
...modelSummary,
cost: costInfo.cost,
priced: costInfo.configured,
+ priceSource: costInfo.configured ? priceInfo.source : "none",
};
})
.sort((a, b) => b.total - a.total || a.model.localeCompare(b.model));
@@ -2969,6 +3078,10 @@
font-size: 11px;
font-weight: 600;
}
+ #${PANEL_ID} .codex-daily-price-clear:disabled {
+ cursor: default;
+ opacity: 0.55;
+ }
#${PANEL_ID} .codex-daily-price-list {
display: grid;
gap: 9px;
@@ -2978,7 +3091,7 @@
}
#${PANEL_ID} .codex-daily-price-row {
display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
+ grid-template-columns: minmax(0, 1fr) auto auto;
gap: 6px;
padding-bottom: 9px;
border-bottom: 1px solid var(--color-token-border, rgba(127, 127, 127, 0.13));
@@ -2995,6 +3108,20 @@
font-size: 11px;
font-weight: 650;
}
+ #${PANEL_ID} .codex-daily-price-badge {
+ justify-self: end;
+ align-self: center;
+ padding: 2px 6px;
+ border-radius: 999px;
+ color: #2676d9;
+ background: rgba(59, 167, 255, 0.12);
+ font-size: 10px;
+ font-weight: 700;
+ }
+ #${PANEL_ID} .codex-daily-price-badge.is-custom {
+ color: #7652e5;
+ background: rgba(141, 107, 255, 0.13);
+ }
#${PANEL_ID} .codex-daily-price-grid {
grid-column: 1 / -1;
display: grid;
@@ -3182,7 +3309,7 @@
Model 价格设置
USD / 1M tokens
-
缓存输入留空按输入价计算;推理 Token 留空按输出价计算。价格只用于本地估算,不代表官方账单。
+
内置 OpenAI API Pricing 的 Standard 参考价,单位 USD / 1M tokens;有上下文分档的模型按短上下文价预置。用户输入会覆盖预置字段。Cache writes、工具调用按次费用、Batch/Flex/Priority 不纳入估算。价格只用于本地估算,不代表官方账单。
@@ -3304,7 +3431,10 @@
const list = panel.querySelector(".codex-daily-model-list");
const meta = panel.querySelector('[data-field="modelMeta"]');
if (meta) {
- const pricedText = snapshot.pricedModels > 0 ? `${snapshot.pricedModels} 个 Model 已定价` : "暂无定价";
+ const sourceParts = [];
+ if (snapshot.customPricedModels) sourceParts.push(`${snapshot.customPricedModels} 个自定义`);
+ if (snapshot.defaultPricedModels) sourceParts.push(`${snapshot.defaultPricedModels} 个预置参考价`);
+ const pricedText = snapshot.pricedModels > 0 ? sourceParts.join(" · ") || `${snapshot.pricedModels} 个 Model 已定价` : "暂无定价";
meta.textContent = `${formatCost(snapshot.cost)} · ${pricedText}`;
}
if (!list) return;
@@ -3317,9 +3447,10 @@
const row = document.createElement("div");
row.className = "codex-daily-model-row";
const percent = Math.max(4, Math.min(100, (toCount(item.total) / maxTotal) * 100));
+ const priceTitle = item.priceSource === "default" ? DEFAULT_OPENAI_PRICE_SOURCE : item.priceSource === "custom" ? "用户自定义价格" : "未配置价格";
row.innerHTML = `
${escapeHtml(item.model)}
-
${item.priced ? formatCost(item.cost) : "未定价"}
+
${item.priced ? formatCost(item.cost) : "未定价"}
@@ -3402,17 +3533,23 @@
}
list.replaceChildren(
...models.map((model) => {
- const entry = normalizePriceEntry(priceConfig.models[model]);
+ const customEntry = normalizePriceEntry(priceConfig.models[model]);
+ const defaultEntry = defaultModelPrice(model);
+ const priceInfo = getModelPriceInfo(model);
+ const badgeText = priceInfo.hasCustom ? "自定义" : priceInfo.hasDefault ? "预置" : "未定价";
+ const clearLabel = priceInfo.hasCustom ? (priceInfo.hasDefault ? "恢复预置" : "清除") : priceInfo.hasDefault ? "预置" : "清除";
+ const clearDisabled = priceInfo.hasCustom ? "" : " disabled";
const row = document.createElement("div");
row.className = "codex-daily-price-row";
row.innerHTML = `
${escapeHtml(model)}
-
+
${badgeText}
+
- ${priceInputHtml(model, "input", "输入", entry.input)}
- ${priceInputHtml(model, "cachedInput", "缓存", entry.cachedInput)}
- ${priceInputHtml(model, "output", "输出", entry.output)}
- ${priceInputHtml(model, "reasoning", "推理", entry.reasoning)}
+ ${priceInputHtml(model, "input", "输入", customEntry.input, defaultEntry?.input)}
+ ${priceInputHtml(model, "cachedInput", "缓存", customEntry.cachedInput, defaultEntry?.cachedInput)}
+ ${priceInputHtml(model, "output", "输出", customEntry.output, defaultEntry?.output)}
+ ${priceInputHtml(model, "reasoning", "推理", customEntry.reasoning, defaultEntry?.reasoning)}
`;
return row;
@@ -3420,12 +3557,12 @@
);
}
- function priceInputHtml(model, field, label, value) {
+ function priceInputHtml(model, field, label, value, placeholderValue = null) {
return `
`;
}
@@ -4142,6 +4279,7 @@
createShareBlob,
copyShareImage,
getModelPrices: () => JSON.parse(JSON.stringify(priceConfig.models)),
+ getDefaultModelPrices: () => JSON.parse(JSON.stringify(DEFAULT_OPENAI_MODEL_PRICES)),
setModelPrice,
clearModelPrice,
resetToday,
@@ -4158,6 +4296,8 @@
recordToolCall,
extractDomToolCallEvents,
processDomToolCalls,
+ defaultModelPrice,
+ getModelPriceInfo,
calculateUsageCost,
formatCost,
getDateKey,