From e215bda925ab4c8ee85ceadf687c06398b8e3f5f Mon Sep 17 00:00:00 2001 From: Rat0323 <261020116+Rat0323@users.noreply.github.com> Date: Sun, 23 Aug 2026 21:17:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=AF=8F=E6=97=A5=20T?= =?UTF-8?q?oken=20=E5=85=A5=E5=8F=A3=E9=A1=B6=E6=A0=8F=E6=BC=82=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.json | 4 +- scripts/codex-daily-token-usage.js | 21 ++- tests/codex-daily-token-usage-layout.test.js | 182 +++++++++++++++++++ 3 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 tests/codex-daily-token-usage-layout.test.js diff --git a/index.json b/index.json index f086d35..adbf7cf 100644 --- a/index.json +++ b/index.json @@ -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", @@ -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", diff --git a/scripts/codex-daily-token-usage.js b/scripts/codex-daily-token-usage.js index 59173d2..f6d6b7e 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.15 +// @version 1.4.16 // @description 每日 Token 统计,近 5 日滚动存储,优先复用已有采集,必要时内置采集,支持 Model 价格、成本估算、日期切换、5 日趋势与分享图。 // @match app://-/* // @run-at document-start @@ -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"; @@ -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"]'; @@ -108,7 +108,8 @@ "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; @@ -116,7 +117,6 @@ 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" }, ]; @@ -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) { diff --git a/tests/codex-daily-token-usage-layout.test.js b/tests/codex-daily-token-usage-layout.test.js new file mode 100644 index 0000000..b8c706f --- /dev/null +++ b/tests/codex-daily-token-usage-layout.test.js @@ -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); +});