Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions index.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"id": "codex-daily-token-usage",
"name": "Codex Daily Token Usage",
"description": "每日 Token 统计,近 5 日滚动存储,自动复用已有采集,支持 Model 价格、成本估算、趋势明细和分享图",
"version": "1.4.15",
"version": "1.4.16",
"author": "kts-kris",
"tags": [
"codex",
Expand All @@ -85,7 +85,7 @@
],
"homepage": "",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/codex-daily-token-usage.js",
"sha256": "5c74b7051e5617a0d7d8e2f15d70870c131260990c3f98e1e5a0c665e39ae37d"
"sha256": "f0544c2ee0f4efd8cbb1a978e2466b6cebca92246509852ff00a9f0936af1845"
},
{
"id": "codex-list-pagebuster",
Expand Down
21 changes: 11 additions & 10 deletions scripts/codex-daily-token-usage.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ==UserScript==
// @name Codex Daily Token Usage
// @namespace codex-plus-plus
// @version 1.4.15
// @version 1.4.16
// @description 每日 Token 统计,近 5 日滚动存储,优先复用已有采集,必要时内置采集,支持 Model 价格、成本估算、日期切换、5 日趋势与分享图。
// @match app://-/*
// @run-at document-start
Expand All @@ -10,7 +10,7 @@
(() => {
"use strict";

const VERSION = "1.4.15";
const VERSION = "1.4.16";
const API_KEY = "__codexDailyTokenUsage";
const SOURCE_API_KEY = "__codexTokenUsage";
const STORAGE_KEY = "__codexDailyTokenUsageV1";
Expand All @@ -19,7 +19,7 @@
const PANEL_ID = "codex-daily-token-usage-panel";
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_SELECTOR = '[class*="ApplicationMenuTopBar"], .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"]';
Expand Down Expand Up @@ -108,15 +108,15 @@
"computer-use-preview": { input: 3, output: 12 },
});
const FLOATING_TOP = 2;
const FLOATING_DEFAULT_RIGHT = 280;
const WINDOW_BUTTON_SAFE_RIGHT = 132;
const FLOATING_DEFAULT_RIGHT = WINDOW_BUTTON_SAFE_RIGHT;
const FLOATING_SAFE_GAP = 8;
const FLOATING_SCAN_TOP = 96;
const FLOATING_MIN_WIDTH = 94;
const FLOATING_COMPACT_WIDTH = 31;
const FLOATING_HEIGHT = 31;
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" },
];
Expand Down Expand Up @@ -4031,11 +4031,12 @@
}

function findAppHeaderElement() {
return (
document.querySelector(APP_HEADER_SELECTOR) ||
document.querySelector(APP_HEADER_SURFACE_SELECTOR) ||
document.querySelector("header")
);
const appHeader = document.querySelector(APP_HEADER_SELECTOR);
if (visibleTopRect(appHeader)) return appHeader;

const menuBar = document.querySelector('[role="menubar"]');
const menuTopBar = menuBar?.closest?.('[class*="ApplicationMenuTopBar"]');
return visibleTopRect(menuTopBar) ? menuTopBar : null;
}

function isTopChromeObstacleNode(node, style) {
Expand Down
182 changes: 182 additions & 0 deletions tests/codex-daily-token-usage-layout.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"use strict";

const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const vm = require("node:vm");

const scriptPath = path.join(__dirname, "..", "scripts", "codex-daily-token-usage.js");
const source = fs.readFileSync(scriptPath, "utf8");

function extractFunction(name) {
const start = source.indexOf(`function ${name}(`);
assert.notEqual(start, -1, `${name} was not found`);
const bodyStart = source.indexOf("{", start);
let depth = 0;
let quote = null;
let escaped = false;
let lineComment = false;
let blockComment = false;

for (let index = bodyStart; index < source.length; index += 1) {
const char = source[index];
const next = source[index + 1];
if (lineComment) {
if (char === "\n") lineComment = false;
continue;
}
if (blockComment) {
if (char === "*" && next === "/") {
blockComment = false;
index += 1;
}
continue;
}
if (quote) {
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === quote) quote = null;
continue;
}
if (char === "/" && next === "/") {
lineComment = true;
index += 1;
continue;
}
if (char === "/" && next === "*") {
blockComment = true;
index += 1;
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
continue;
}
if (char === "{") depth += 1;
if (char === "}") depth -= 1;
if (depth === 0) return source.slice(start, index + 1);
}
throw new Error(`Could not extract ${name}`);
}

function numericConstant(name) {
const match = source.match(new RegExp(`const ${name} = (\\d+);`));
assert.ok(match, `${name} was not found`);
return Number(match[1]);
}

function visibleNode(rect) {
return {
getBoundingClientRect() {
return rect;
},
};
}

test("selects the application menu top bar instead of a conversation header", () => {
const appHeader = visibleNode({ left: 0, top: 0, right: 1708, bottom: 36 });
const conversationHeader = visibleNode({ left: 308, top: 36, right: 1708, bottom: 82 });
const queried = [];
const context = {
APP_HEADER_SELECTOR: '[class*="ApplicationMenuTopBar"], .app-header-tint',
FLOATING_SCAN_TOP: numericConstant("FLOATING_SCAN_TOP"),
document: {
querySelector(selector) {
queried.push(selector);
if (selector === context.APP_HEADER_SELECTOR) return appHeader;
if (selector === "header") return conversationHeader;
return null;
},
},
getComputedStyle() {
return { display: "flex", visibility: "visible", opacity: "1" };
},
};
const findAppHeaderElement = vm.runInNewContext(
`${extractFunction("normalizeRect")}\n${extractFunction("visibleTopRect")}\n${extractFunction("findAppHeaderElement")}\nfindAppHeaderElement`,
context,
);

assert.equal(findAppHeaderElement(), appHeader);
assert.doesNotMatch(extractFunction("findAppHeaderElement"), /querySelector\(["']header["']\)/);
assert.deepEqual(queried, [context.APP_HEADER_SELECTOR]);
});

test("falls back to the application menu bar without selecting a conversation header", () => {
const appHeader = visibleNode({ left: 0, top: 0, right: 1708, bottom: 36 });
const menuBar = {
closest(selector) {
assert.equal(selector, '[class*="ApplicationMenuTopBar"]');
return appHeader;
},
};
const context = {
APP_HEADER_SELECTOR: '[class*="ApplicationMenuTopBar"], .app-header-tint',
FLOATING_SCAN_TOP: numericConstant("FLOATING_SCAN_TOP"),
document: {
querySelector(selector) {
if (selector === context.APP_HEADER_SELECTOR) return null;
if (selector === '[role="menubar"]') return menuBar;
if (selector === "header") throw new Error("conversation header must not be queried");
return null;
},
},
getComputedStyle() {
return { display: "flex", visibility: "visible", opacity: "1" };
},
};
const findAppHeaderElement = vm.runInNewContext(
`${extractFunction("normalizeRect")}\n${extractFunction("visibleTopRect")}\n${extractFunction("findAppHeaderElement")}\nfindAppHeaderElement`,
context,
);

assert.equal(findAppHeaderElement(), appHeader);
});

test("keeps the daily trigger in the window title bar when no toolbar anchor exists", () => {
assert.match(
source,
/const FLOATING_DEFAULT_RIGHT = WINDOW_BUTTON_SAFE_RIGHT;/,
"the fallback position must reserve only the native window controls",
);

const context = {
FLOATING_COMPACT_WIDTH: numericConstant("FLOATING_COMPACT_WIDTH"),
FLOATING_DEFAULT_RIGHT: numericConstant("WINDOW_BUTTON_SAFE_RIGHT"),
FLOATING_HEIGHT: numericConstant("FLOATING_HEIGHT"),
FLOATING_MIN_WIDTH: numericConstant("FLOATING_MIN_WIDTH"),
FLOATING_SAFE_GAP: numericConstant("FLOATING_SAFE_GAP"),
FLOATING_TOP: numericConstant("FLOATING_TOP"),
PANEL_MARGIN: numericConstant("PANEL_MARGIN"),
};
const resolveFloatingLayout = vm.runInNewContext(
[
extractFunction("normalizeRect"),
extractFunction("rectsOverlap"),
extractFunction("candidateRectFromRight"),
extractFunction("normalizeFloatingAnchor"),
extractFunction("resolveFloatingLayout"),
"resolveFloatingLayout",
].join("\n"),
context,
);

const layout = resolveFloatingLayout(108, 31, 1708, 1020, [], []);

assert.deepEqual(
JSON.parse(JSON.stringify(layout)),
{ top: 2, right: 132, left: 1468, width: 108, compact: false },
);
});

test("publishes matching script version and checksum metadata", () => {
const index = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "index.json"), "utf8"));
const entry = index.scripts.find((script) => script.id === "codex-daily-token-usage");
const version = source.match(/const VERSION = "([^"]+)";/)?.[1];
const sha256 = crypto.createHash("sha256").update(fs.readFileSync(scriptPath)).digest("hex");

assert.equal(entry.version, version);
assert.equal(entry.sha256, sha256);
});