codex-ringside 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +19 -3
- package/README.zh-CN.md +20 -4
- package/package.json +6 -2
- package/src/agent.cjs +1 -0
- package/src/card.js +258 -40
- package/src/limits.cjs +30 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-11-03)
|
|
4
|
+
|
|
5
|
+
- Replace the native sidebar usage icon with Ringside meters in place, preserving its usage menu, management actions and keyboard controls. Restore the native icon when the add-on is removed.
|
|
6
|
+
- Show plan, credit balance (including zero/unlimited), reset times and reset cards in the hover details. Clear all account-specific values on expiry, disconnection and account changes.
|
|
7
|
+
- Preserve the original background-settings action on ring click, Enter and Space, including interception of the native pointerdown menu. Keep the native usage menu under a separate hover-details action and ArrowDown; right-click and Shift+F10 still open background settings.
|
|
8
|
+
- Add Chromium interaction coverage for native replacement, remounting, cleanup, menu actions, background settings and stale/credit-only states.
|
|
9
|
+
|
|
3
10
|
## 0.2.0 (2026-10-31)
|
|
4
11
|
|
|
5
12
|
- Automatically check stable GitHub releases every six hours, verify and install newer standalone bundles, and retry failures after one hour. Preserve settings, back up and roll back failed updates without closing the desktop client.
|
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@ English · [简体中文](README.zh-CN.md)
|
|
|
4
4
|
|
|
5
5
|
Usage rings and customizable backgrounds for the Codex desktop sidebar on macOS.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Rings replace the native sidebar usage icon in place. Hover for your plan, remaining allowance, credit balance, reset times and reset cards; click to open the original background settings and configure a wallpaper, image or glass background. Image controls include cropping, positioning, zoom, blur and color adjustments. The hover details' usage-menu button preserves the native menu and its management actions.
|
|
8
|
+
|
|
9
|
+
Credit balances are grouped and rounded to whole credits, including zero; unlimited balances are labeled explicitly, and missing balances are omitted. Credit-only accounts show a balance summary instead of a percentage ring. Account changes, connection failures and data older than 150 seconds clear old account values. On older clients without a compatible usage button, a separate ring opens background settings on click. Removing the add-on restores the native icon.
|
|
8
10
|
|
|
9
11
|
**Unofficial community add-on. Not affiliated with or endorsed by OpenAI.** Parts are adapted from [Codex Usage Card](https://github.com/Lorie7813/codex-usage-card); see [NOTICE](NOTICE).
|
|
10
12
|
|
|
@@ -102,7 +104,7 @@ Reinstalling preserves observe mode; `--observe` explicitly disables startup han
|
|
|
102
104
|
|
|
103
105
|
## Background controls
|
|
104
106
|
|
|
105
|
-
Click a ring to open settings. Panel changes apply immediately; CLI changes apply within five seconds.
|
|
107
|
+
Click a ring, or press Enter or Space while focused, to open background settings. The hover details' background settings button, right-click and Shift+F10 also work. Open the native usage menu through the separate button in the hover details or ArrowDown while the ring is focused; this entry is hidden on clients without a compatible native usage button. Panel changes apply immediately; CLI changes apply within five seconds.
|
|
106
108
|
|
|
107
109
|
| Source | Behavior |
|
|
108
110
|
| --- | --- |
|
|
@@ -132,13 +134,27 @@ Options without a source keep the current source. `background` without arguments
|
|
|
132
134
|
- Background cleanup uses host DOM heuristics. Terminals, code blocks, canvases and embedded web content are left alone; an unfamiliar page may retain opaque areas.
|
|
133
135
|
- Host updates can break rate-limit reading, placement or theme integration. Disable the background or uninstall if problems arise.
|
|
134
136
|
|
|
137
|
+
### Recommended wallpaper sites
|
|
138
|
+
|
|
139
|
+
Find a background image on these sites:
|
|
140
|
+
|
|
141
|
+
| Site | Highlights |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| [Wallhaven](https://wallhaven.cc/) | A broad wallpaper collection with keyword, resolution and aspect-ratio filters |
|
|
144
|
+
| [WallpaperHub](https://www.wallpaperhub.app/) | Windows, Surface and Fluent Design wallpapers in sizes for different devices |
|
|
145
|
+
| [Peapix](https://peapix.com/) | Bing daily wallpapers and Windows Spotlight images, including landscapes, architecture and nature photography |
|
|
146
|
+
| [Unsplash](https://unsplash.com/wallpapers) | HD photography wallpapers covering nature, travel, animals and more |
|
|
147
|
+
| [Pexels](https://www.pexels.com/search/desktop%20wallpaper/) | A photography library with desktop, 4K and dark wallpaper categories |
|
|
148
|
+
|
|
149
|
+
Download an image to your Mac, click a sidebar ring, then choose the `image` source and select the file. You can also set it as your macOS desktop wallpaper and choose the `wallpaper` source to align the background with your desktop.
|
|
150
|
+
|
|
135
151
|
## Status and troubleshooting
|
|
136
152
|
|
|
137
153
|
```bash
|
|
138
154
|
"$HOME/Library/Application Support/CodexRingside/bin/codex-ringside" status
|
|
139
155
|
```
|
|
140
156
|
|
|
141
|
-
Look for `agent: "running"`, `cdp.windows[].card.placed`, and `cdp.windows[].background.applied`. `nativeGlass: false` means the host disables native glass. `startup.event` explains skipped handoffs. Status may contain private paths and window URLs; redact it before sharing.
|
|
157
|
+
Look for `agent: "running"`, `cdp.windows[].card.placed`, and `cdp.windows[].background.applied`. `card.replacesNative` confirms replacement of the native usage icon; `card.credits` and `card.plan` show current allowance details. `nativeGlass: false` means the host disables native glass. `startup.event` explains skipped handoffs. Status may contain private paths and window URLs; redact it before sharing.
|
|
142
158
|
|
|
143
159
|
If no ring appears, verify the signed-in host, application path and whether port 39222 is occupied. Node and Command Line Tools checks apply only to npm/source installations. A missing release asset means that version does not contain a standalone bundle; use 0.2.0 or later. A checksum or download failure stops before changing the installed app; retry when the network is available. Installing while the host is open does not trigger a restart. Save your work and reopen its original icon when convenient. Check `~/Library/Logs/CodexRingside` for errors.
|
|
144
160
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,19 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
用剩余额度圆环替换 Codex 桌面端侧栏的官方用量图标,悬停可看套餐、余额和重置时间,点击圆环打开原有背景设置。可以自定义图片、壁纸或毛玻璃,透明度和模糊自由调节,图片还能裁剪、缩放和调整布局。悬停详情中的“用量菜单”保留官方“管理用量”等操作。仅支持 macOS。
|
|
8
8
|
|
|
9
9
|
这是非官方插件,与 OpenAI 无关。原理参考 [Codex Usage Card](https://github.com/Lorie7813/codex-usage-card),署名见 [NOTICE](NOTICE)。
|
|
10
10
|
|
|
11
11
|
## 工作原理
|
|
12
12
|
|
|
13
13
|
- **额度**:启动 Codex 内置的 `codex app-server`,通过 stdio 调用 `account/rateLimits/read`,每 60 秒刷新一次。
|
|
14
|
-
- **显示**:通过本机调试端口 `127.0.0.1:39222`
|
|
14
|
+
- **显示**:通过本机调试端口 `127.0.0.1:39222` 在侧栏官方用量按钮内替换图标,保留原生菜单、键盘操作和“管理用量”等账号功能。找不到兼容的官方按钮时,回退为独立圆环;卸载会恢复原图标。
|
|
15
15
|
- **背景**:同样通过调试端口,把 Codex 外壳层的背景清空,在页面最底层放一张图片。窗口属性由主进程控制,插件改不了,所以“透明”透出的是 Codex 自带的系统毛玻璃,壁纸来源显示的是对齐到屏幕位置的桌面壁纸。
|
|
16
16
|
- **启动**:Codex 的 `app.asar` 受完整性校验保护,不能修改,只能让它带调试端口启动。后台任务发现你从原图标打开 Codex 时,会在安全条件都满足的情况下有序退出,再带端口重开一次。
|
|
17
17
|
|
|
18
18
|
账号有几个额度窗口就显示几个圆环。圆环是分段刻度样式,环内显示剩余百分比和窗口名称(“5小时”或“周”)。额度充足时为青绿渐变,50% 及以下转为黄橙,低于 10% 转为橙红。
|
|
19
19
|
|
|
20
|
+
悬停详情显示套餐、各窗口的剩余百分比及重置时间、可用重置卡,以及接口提供的额度余额。余额按整数分组显示,例如 `12,346`;零余额显示 `0`,无限额度显示“不限额”,未返回余额时不猜测数值。只有余额、没有百分比窗口时,侧栏显示余额摘要。连接失败、数据超过 150 秒未更新或账号切换时,会清除旧数值并提示重试。重置、购买和管理操作继续由官方菜单处理。
|
|
21
|
+
|
|
20
22
|
## 安装
|
|
21
23
|
|
|
22
24
|
一键安装需要 **macOS 13 或更高版本**(Apple Silicon 或 Intel),以及已登录的兼容桌面客户端。**无需自行安装 Node.js、npm、Homebrew 或 Xcode Command Line Tools。** 安装包自带专用 Node 和预编译 helper。开发验证使用 `/Applications/ChatGPT.app`,版本 `26.1027.30824`;其他客户端版本依赖相同内部结构,未逐一验证。命令行和设置面板目前使用中文。
|
|
@@ -111,7 +113,7 @@ npx codex-ringside@latest install
|
|
|
111
113
|
|
|
112
114
|
## 自定义背景
|
|
113
115
|
|
|
114
|
-
|
|
116
|
+
直接点击侧栏圆环打开原有背景设置面板,聚焦后按 Enter 或空格也可以打开。悬停面板中的“背景设置”、右键和 Shift+F10 仍可使用。官方用量菜单通过悬停面板中的“用量菜单”打开,也可以聚焦圆环后按向下键;没有兼容的官方按钮时,这个菜单入口隐藏。先选来源,再用效果调外观,所有调节都立即生效。
|
|
115
117
|
|
|
116
118
|
面板顶部还能切换 Codex 的**外观模式**:跟随系统、浅色、深色。它和设置页的“模式”是同一个设置,改动会保存,并在两边同步。这里用的是 Codex 内部的设置函数,找不到时控件自动隐藏。
|
|
117
119
|
|
|
@@ -163,6 +165,20 @@ npx codex-ringside@latest install
|
|
|
163
165
|
- **切换有延迟**:切换桌面空间后,壁纸最多 60 秒才更新;拖动窗口时壁纸有轻微跟随延迟。
|
|
164
166
|
- **页面自动适配**:内容区里方角、无边框、占满区域的不透明层会被当作页面背景清除,设置页这类圆角卡片变成半透明磨砂,文字依然清楚。终端、代码块和内嵌网页不处理。设计不同的页面可能漏掉,这时自带背景的地方保持原样。
|
|
165
167
|
|
|
168
|
+
### 壁纸网站推荐
|
|
169
|
+
|
|
170
|
+
可以从这些网站挑选背景图片:
|
|
171
|
+
|
|
172
|
+
| 网站 | 推荐内容 |
|
|
173
|
+
|---|---|
|
|
174
|
+
| [Wallhaven](https://wallhaven.cc/) | 综合壁纸图库,可按关键词、分辨率和宽高比筛选 |
|
|
175
|
+
| [WallpaperHub](https://www.wallpaperhub.app/) | Windows、Surface 和 Fluent Design 风格壁纸,提供多种设备尺寸 |
|
|
176
|
+
| [Peapix](https://peapix.com/) | Bing 每日壁纸与 Windows Spotlight 图片,适合寻找风景、建筑和自然摄影 |
|
|
177
|
+
| [Unsplash](https://unsplash.com/wallpapers) | 摄影类高清壁纸,涵盖自然、旅行、动物等主题 |
|
|
178
|
+
| [Pexels](https://www.pexels.com/search/desktop%20wallpaper/) | 摄影图库,提供桌面、4K 和深色壁纸等分类 |
|
|
179
|
+
|
|
180
|
+
下载图片到本机后,点击侧栏圆环,在背景设置面板中选择 `image` 图片来源并选取文件。也可以先将图片设为 macOS 桌面壁纸,再选择 `wallpaper` 壁纸来源,让背景与桌面对齐。
|
|
181
|
+
|
|
166
182
|
## 查看状态
|
|
167
183
|
|
|
168
184
|
下载失败或 SHA-256 校验失败时,安装器会在更改现有安装前停止,可在网络恢复后重试。发行版缺少安装包时,请选择 0.2.0 或更高版本。通过一键入口安装后,无需检查系统 Node 或编译工具。
|
|
@@ -171,7 +187,7 @@ npx codex-ringside@latest install
|
|
|
171
187
|
"$HOME/Library/Application Support/CodexRingside/bin/codex-ringside" status
|
|
172
188
|
```
|
|
173
189
|
|
|
174
|
-
重点看 `cdp.windows[].card.placed
|
|
190
|
+
重点看 `cdp.windows[].card.placed`,`replacesNative` 为 `true` 表示已替换官方用量图标;`credits` 和 `plan` 显示当前余额和套餐。背景看 `background`(保存的设置)和 `cdp.windows[].background`:`applied` 表示已生效,`nativeGlass` 为 `false` 表示 Codex 此刻关闭了毛玻璃。输出可能包含本机路径,分享前请先脱敏。
|
|
175
191
|
|
|
176
192
|
## 卸载
|
|
177
193
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-ringside",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Usage rings and customizable backgrounds for the Codex desktop app. Unofficial macOS add-on.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test test/*.test.cjs",
|
|
18
|
+
"test:card": "node --test test/card.browser.cjs",
|
|
18
19
|
"install-agent": "node bin/codex-ringside.cjs install",
|
|
19
20
|
"uninstall-agent": "node bin/codex-ringside.cjs uninstall",
|
|
20
21
|
"status": "node bin/codex-ringside.cjs status",
|
|
@@ -53,5 +54,8 @@
|
|
|
53
54
|
"LICENSE",
|
|
54
55
|
"NOTICE",
|
|
55
56
|
"CHANGELOG.md"
|
|
56
|
-
]
|
|
57
|
+
],
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"playwright": "1.64.1"
|
|
60
|
+
}
|
|
57
61
|
}
|
package/src/agent.cjs
CHANGED
package/src/card.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Runs inside the Codex renderer. Must stay self-contained: cdp.cjs injects this file as text.
|
|
2
2
|
function installCodexRingsideCard() {
|
|
3
|
-
const VERSION =
|
|
3
|
+
const VERSION = 17;
|
|
4
4
|
const KEY = '__codexRingsideCard';
|
|
5
5
|
const STALE_AFTER_MS = 150000;
|
|
6
6
|
const TICKS = 30;
|
|
@@ -18,14 +18,14 @@ function installCodexRingsideCard() {
|
|
|
18
18
|
const card = document.createElement('div');
|
|
19
19
|
card.id = 'codex-ringside-card';
|
|
20
20
|
card.tabIndex = 0;
|
|
21
|
-
card.setAttribute('role', '
|
|
21
|
+
card.setAttribute('role', 'button');
|
|
22
22
|
card.setAttribute('aria-label', 'Codex 剩余额度');
|
|
23
|
-
card.setAttribute('aria-describedby', 'codex-ringside-tooltip');
|
|
24
23
|
card.setAttribute('aria-haspopup', 'dialog');
|
|
25
24
|
|
|
26
25
|
const tooltip = document.createElement('div');
|
|
27
26
|
tooltip.id = 'codex-ringside-tooltip';
|
|
28
|
-
tooltip.setAttribute('role', '
|
|
27
|
+
tooltip.setAttribute('role', 'dialog');
|
|
28
|
+
tooltip.setAttribute('aria-label', 'Codex 剩余额度');
|
|
29
29
|
tooltip.hidden = true;
|
|
30
30
|
|
|
31
31
|
const style = document.createElement('style');
|
|
@@ -47,12 +47,16 @@ function installCodexRingsideCard() {
|
|
|
47
47
|
/* The spread also grows the corners, so the outer radius is 4 + 3 = 7px; the width matches the rail buttons' 36px. */
|
|
48
48
|
#codex-ringside-card:hover { background: var(--ringside-hover); box-shadow: 0 0 0 3px var(--ringside-hover); }
|
|
49
49
|
#codex-ringside-card:active { transform: scale(.96); }
|
|
50
|
+
/* Keep the native menu available, with Ringside's original background-settings click. */
|
|
51
|
+
[data-codex-ringside-host] { height: auto !important; min-height: 36px; padding: 3px !important; }
|
|
52
|
+
[data-codex-ringside-host] > :not(#codex-ringside-card) { display: none !important; }
|
|
53
|
+
[data-codex-ringside-host] > #codex-ringside-card { margin: 0; pointer-events: none; }
|
|
50
54
|
@media (prefers-reduced-motion: reduce) { #codex-ringside-card { transition: none; } }
|
|
51
55
|
html.dark #codex-ringside-card, html[data-theme="dark"] #codex-ringside-card { ${dark} }
|
|
52
56
|
@media (prefers-color-scheme: dark) {
|
|
53
57
|
html:not(.light):not([data-theme="light"]) #codex-ringside-card { ${dark} }
|
|
54
58
|
}
|
|
55
|
-
#codex-ringside-card[hidden], #codex-ringside-tooltip[hidden] { display: none !important; }
|
|
59
|
+
#codex-ringside-card[hidden], #codex-ringside-tooltip[hidden], #codex-ringside-tooltip [hidden] { display: none !important; }
|
|
56
60
|
#codex-ringside-card:focus-visible { outline: 1px solid var(--ringside-text); outline-offset: 2px; }
|
|
57
61
|
#codex-ringside-card .ringside-meter { position: relative; flex: 0 0 auto; width: 30px; height: 30px; }
|
|
58
62
|
#codex-ringside-card .ringside-meter[data-tone="normal"] { --ringside-glow: #34e08280; }
|
|
@@ -67,11 +71,12 @@ function installCodexRingsideCard() {
|
|
|
67
71
|
#codex-ringside-card .ringside-number { font-size: 8px; line-height: 9px; font-weight: 600; letter-spacing: -.3px; white-space: nowrap; }
|
|
68
72
|
#codex-ringside-card .ringside-label { font-size: 6.5px; line-height: 8px; color: var(--ringside-label); white-space: nowrap; }
|
|
69
73
|
#codex-ringside-tooltip {
|
|
70
|
-
box-sizing: border-box; position: fixed; z-index: 2147483000; min-width:
|
|
71
|
-
padding: 12px 14px; border: 1px solid color-mix(in srgb, currentColor 12%, transparent); border-radius:
|
|
74
|
+
box-sizing: border-box; position: fixed; z-index: 2147483000; min-width: 204px; max-width: min(320px, calc(100vw - 16px));
|
|
75
|
+
padding: 12px 14px; border: 1px solid color-mix(in srgb, currentColor 12%, transparent); border-radius: 12px;
|
|
72
76
|
background: var(--color-surface-elevated-secondary, #f8f8f7); color: var(--color-text-primary, #303630);
|
|
73
|
-
box-shadow: 0
|
|
74
|
-
|
|
77
|
+
box-shadow: 0 6px 22px #00000024; -webkit-app-region: no-drag;
|
|
78
|
+
max-height: calc(100vh - 16px); overflow: auto;
|
|
79
|
+
font: 12px/1.45 -apple-system, BlinkMacSystemFont, sans-serif;
|
|
75
80
|
}
|
|
76
81
|
html.dark #codex-ringside-tooltip, html[data-theme="dark"] #codex-ringside-tooltip {
|
|
77
82
|
background: var(--color-surface-elevated-secondary, #2c2e2c); color: var(--color-text-primary, #edf0ed);
|
|
@@ -81,21 +86,53 @@ function installCodexRingsideCard() {
|
|
|
81
86
|
background: var(--color-surface-elevated-secondary, #2c2e2c); color: var(--color-text-primary, #edf0ed);
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
|
-
#codex-ringside-tooltip .ringside-
|
|
85
|
-
#codex-ringside-tooltip .ringside-
|
|
86
|
-
#codex-ringside-tooltip .ringside-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
#codex-ringside-tooltip .ringside-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
90
|
+
#codex-ringside-tooltip .ringside-title { font-weight: 600; font-size: 12.5px; }
|
|
91
|
+
#codex-ringside-tooltip .ringside-badge {
|
|
92
|
+
padding: 1px 7px; border-radius: 999px; font-size: 10.5px; line-height: 16px; font-weight: 500;
|
|
93
|
+
background: color-mix(in srgb, currentColor 9%, transparent); color: color-mix(in srgb, currentColor 80%, transparent); white-space: nowrap;
|
|
94
|
+
}
|
|
95
|
+
#codex-ringside-tooltip .ringside-sections { display: flex; flex-wrap: wrap; gap: 10px 20px; margin-top: 10px; }
|
|
96
|
+
#codex-ringside-tooltip .ringside-section { flex: 1 1 auto; min-width: 72px; }
|
|
97
|
+
#codex-ringside-tooltip .ringside-section-percent {
|
|
98
|
+
margin: 2px 0; font-size: 24px; line-height: 28px; font-weight: 700; letter-spacing: -.02em; font-variant-numeric: tabular-nums;
|
|
99
|
+
}
|
|
100
|
+
#codex-ringside-tooltip .ringside-dim { opacity: .68; font-size: 11px; }
|
|
101
|
+
#codex-ringside-tooltip .ringside-message { margin-top: 8px; }
|
|
102
|
+
#codex-ringside-tooltip .ringside-footer { margin-top: 10px; padding-top: 9px; border-top: 1px solid color-mix(in srgb, currentColor 10%, transparent); }
|
|
103
|
+
#codex-ringside-tooltip .ringside-meta { display: flex; flex-direction: column; gap: 6px; }
|
|
104
|
+
#codex-ringside-tooltip .ringside-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
105
|
+
#codex-ringside-tooltip .ringside-row[data-label]::before { content: attr(data-label); opacity: .68; font-size: 11px; }
|
|
106
|
+
#codex-ringside-tooltip .ringside-credit-balance { font-size: 14px; line-height: 18px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
107
|
+
#codex-ringside-tooltip .ringside-meta-value { font-size: 12px; line-height: 16px; font-weight: 500; opacity: .9; font-variant-numeric: tabular-nums; }
|
|
108
|
+
#codex-ringside-tooltip .ringside-sr { font-size: 0; opacity: 0; }
|
|
109
|
+
#codex-ringside-tooltip .ringside-actions { display: flex; gap: 6px; }
|
|
110
|
+
#codex-ringside-tooltip button {
|
|
111
|
+
flex: 1; padding: 5px 10px; border: 1px solid color-mix(in srgb, currentColor 14%, transparent); border-radius: 6px;
|
|
112
|
+
font: 500 11.5px/1.35 -apple-system, BlinkMacSystemFont, sans-serif; text-align: center; white-space: nowrap;
|
|
113
|
+
color: inherit; background: color-mix(in srgb, currentColor 4%, transparent); cursor: pointer;
|
|
114
|
+
transition: background-color 120ms ease, border-color 120ms ease;
|
|
115
|
+
}
|
|
116
|
+
#codex-ringside-tooltip button:hover, #codex-ringside-tooltip button:focus-visible {
|
|
117
|
+
background: color-mix(in srgb, currentColor 11%, transparent);
|
|
118
|
+
border-color: color-mix(in srgb, currentColor 22%, transparent);
|
|
119
|
+
}
|
|
120
|
+
#codex-ringside-tooltip button:focus-visible { outline: 1px solid currentColor; outline-offset: 1px; }
|
|
89
121
|
`;
|
|
90
122
|
|
|
91
123
|
let value = { rings: [], message: '正在读取 Codex 剩余额度', note: null, stale: false, updatedAt: null };
|
|
92
124
|
let rail = null;
|
|
125
|
+
let nativeTrigger = null;
|
|
126
|
+
let eventTarget = null;
|
|
93
127
|
let placementTimer = null;
|
|
94
128
|
let hoverTimer = null;
|
|
129
|
+
let hideTimer = null;
|
|
130
|
+
let suppressFocusTooltip = false;
|
|
131
|
+
let forwardingNativeAction = false;
|
|
95
132
|
let disposed = false;
|
|
96
133
|
|
|
97
|
-
const element = (className, text) => {
|
|
98
|
-
const node = document.createElement(
|
|
134
|
+
const element = (className, text, tag = 'div') => {
|
|
135
|
+
const node = document.createElement(tag);
|
|
99
136
|
node.className = className;
|
|
100
137
|
if (text != null) node.textContent = text;
|
|
101
138
|
return node;
|
|
@@ -106,6 +143,25 @@ function installCodexRingsideCard() {
|
|
|
106
143
|
return rect.width > 0 && rect.height > 0 && getComputedStyle(node).visibility !== 'hidden';
|
|
107
144
|
};
|
|
108
145
|
const percentText = percent => (Number.isFinite(percent) ? `${percent}%` : '—');
|
|
146
|
+
const details = element('ringside-details');
|
|
147
|
+
const actions = element('ringside-actions ringside-footer');
|
|
148
|
+
const settingsButton = document.createElement('button');
|
|
149
|
+
settingsButton.type = 'button';
|
|
150
|
+
settingsButton.textContent = '背景设置';
|
|
151
|
+
settingsButton.addEventListener('click', togglePanel);
|
|
152
|
+
const usageButton = document.createElement('button');
|
|
153
|
+
usageButton.type = 'button';
|
|
154
|
+
usageButton.textContent = '用量菜单';
|
|
155
|
+
usageButton.setAttribute('aria-haspopup', 'menu');
|
|
156
|
+
usageButton.addEventListener('click', openUsageMenu);
|
|
157
|
+
actions.append(settingsButton, usageButton);
|
|
158
|
+
tooltip.append(details, actions);
|
|
159
|
+
|
|
160
|
+
function renderActions() {
|
|
161
|
+
settingsButton.hidden = !window.__codexRingsideBackground?.togglePanel;
|
|
162
|
+
usageButton.hidden = !nativeTrigger;
|
|
163
|
+
actions.hidden = settingsButton.hidden && usageButton.hidden;
|
|
164
|
+
}
|
|
109
165
|
|
|
110
166
|
// Filled ticks run clockwise from 12 o'clock; the unused part of the circle stays a solid track.
|
|
111
167
|
function ringSvg(percent, tone) {
|
|
@@ -151,19 +207,53 @@ function installCodexRingsideCard() {
|
|
|
151
207
|
if (ring.resetsAt) section.append(element('ringside-dim', `${ring.resetsAt} 重置`));
|
|
152
208
|
sections.append(section);
|
|
153
209
|
}
|
|
154
|
-
const
|
|
210
|
+
const header = element('ringside-header');
|
|
211
|
+
header.append(element('ringside-title', 'Codex 剩余额度'));
|
|
212
|
+
if (value.plan) header.append(element('ringside-badge', `${value.plan} 套餐`));
|
|
213
|
+
const lines = [header];
|
|
155
214
|
if (sections.childElementCount) lines.push(sections);
|
|
156
|
-
if (value.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
215
|
+
if (value.credits || value.note) {
|
|
216
|
+
const meta = element('ringside-meta ringside-footer');
|
|
217
|
+
if (value.credits) {
|
|
218
|
+
const credits = element('ringside-row');
|
|
219
|
+
credits.append(element('ringside-dim', '额度余额'), element('ringside-credit-balance', value.credits.label));
|
|
220
|
+
meta.append(credits);
|
|
221
|
+
}
|
|
222
|
+
if (value.note) {
|
|
223
|
+
const parts = /^([^::]+)([::]\s*)(.+)$/.exec(value.note);
|
|
224
|
+
if (parts) {
|
|
225
|
+
const note = element('ringside-row');
|
|
226
|
+
note.dataset.label = parts[1];
|
|
227
|
+
const val = element('ringside-meta-value', parts[3], 'span');
|
|
228
|
+
val.prepend(element('ringside-sr', `${parts[1]}${parts[2]}`, 'span'));
|
|
229
|
+
note.append(val);
|
|
230
|
+
meta.append(note);
|
|
231
|
+
} else {
|
|
232
|
+
meta.append(element('ringside-dim', value.note));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
lines.push(meta);
|
|
160
236
|
}
|
|
161
|
-
|
|
237
|
+
if (value.message) lines.push(element('ringside-dim ringside-message', value.message));
|
|
238
|
+
details.replaceChildren(...lines);
|
|
239
|
+
renderActions();
|
|
162
240
|
}
|
|
163
241
|
|
|
164
242
|
function render() {
|
|
165
|
-
|
|
166
|
-
|
|
243
|
+
if (!value.rings?.length && value.credits) {
|
|
244
|
+
const balance = element('ringside-center');
|
|
245
|
+
const compact = value.credits.unlimited ? '∞' : new Intl.NumberFormat('zh-CN', {
|
|
246
|
+
notation: 'compact', maximumFractionDigits: 1
|
|
247
|
+
}).format(value.credits.balance);
|
|
248
|
+
balance.append(element('ringside-number', compact), element('ringside-label', '额度'));
|
|
249
|
+
const meter = element('ringside-meter');
|
|
250
|
+
meter.setAttribute('aria-label', `额度余额:${value.credits.label}`);
|
|
251
|
+
meter.append(balance);
|
|
252
|
+
card.replaceChildren(meter);
|
|
253
|
+
} else {
|
|
254
|
+
const rings = value.rings?.length ? value.rings : [{ label: '额度', name: '额度', percent: null, tone: 'muted' }];
|
|
255
|
+
card.replaceChildren(...rings.map(buildMeter));
|
|
256
|
+
}
|
|
167
257
|
renderTooltip();
|
|
168
258
|
if (!tooltip.hidden) positionTooltip();
|
|
169
259
|
}
|
|
@@ -174,6 +264,9 @@ function installCodexRingsideCard() {
|
|
|
174
264
|
value = {
|
|
175
265
|
...value,
|
|
176
266
|
stale: true,
|
|
267
|
+
credits: null,
|
|
268
|
+
plan: null,
|
|
269
|
+
note: null,
|
|
177
270
|
message: '额度数据已过期,正在等待重新连接',
|
|
178
271
|
rings: (value.rings ?? []).map(ring => ({ ...ring, percent: null, tone: 'muted', resetsAt: null }))
|
|
179
272
|
};
|
|
@@ -184,31 +277,121 @@ function installCodexRingsideCard() {
|
|
|
184
277
|
const panelOpen = () => document.getElementById('codex-ringside-panel')?.hidden === false;
|
|
185
278
|
function togglePanel() {
|
|
186
279
|
hideTooltip();
|
|
187
|
-
window.__codexRingsideBackground?.togglePanel(card);
|
|
280
|
+
window.__codexRingsideBackground?.togglePanel(nativeTrigger ?? card);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function openUsageMenu() {
|
|
284
|
+
if (!nativeTrigger?.isConnected) return;
|
|
285
|
+
if (panelOpen()) window.__codexRingsideBackground?.togglePanel(nativeTrigger);
|
|
286
|
+
hideTooltip();
|
|
287
|
+
// Current clients open on pointerdown; older triggers may open on click. Pass both
|
|
288
|
+
// through the untouched native handlers, only for this explicit menu action.
|
|
289
|
+
forwardingNativeAction = true;
|
|
290
|
+
try {
|
|
291
|
+
nativeTrigger.focus();
|
|
292
|
+
const pointer = { bubbles: true, cancelable: true, button: 0, pointerType: 'mouse', isPrimary: true };
|
|
293
|
+
nativeTrigger.dispatchEvent(new PointerEvent('pointerdown', { ...pointer, buttons: 1 }));
|
|
294
|
+
nativeTrigger.dispatchEvent(new PointerEvent('pointerup', { ...pointer, buttons: 0 }));
|
|
295
|
+
nativeTrigger.click();
|
|
296
|
+
} finally {
|
|
297
|
+
forwardingNativeAction = false;
|
|
298
|
+
hideTooltip();
|
|
299
|
+
}
|
|
188
300
|
}
|
|
189
301
|
|
|
190
302
|
function positionTooltip() {
|
|
191
|
-
const rect = card.getBoundingClientRect();
|
|
303
|
+
const rect = (nativeTrigger ?? card).getBoundingClientRect();
|
|
192
304
|
tooltip.style.left = `${Math.max(8, Math.min(rect.right + 12, innerWidth - tooltip.offsetWidth - 8))}px`;
|
|
193
305
|
const top = rect.top + rect.height / 2 - tooltip.offsetHeight / 2;
|
|
194
306
|
tooltip.style.top = `${Math.max(8, Math.min(top, innerHeight - tooltip.offsetHeight - 8))}px`;
|
|
195
307
|
}
|
|
196
308
|
function hideTooltip() {
|
|
197
309
|
clearTimeout(hoverTimer);
|
|
310
|
+
clearTimeout(hideTimer);
|
|
198
311
|
hoverTimer = null;
|
|
199
312
|
tooltip.hidden = true;
|
|
200
313
|
}
|
|
201
314
|
function showTooltip() {
|
|
202
315
|
clearTimeout(hoverTimer);
|
|
203
|
-
|
|
316
|
+
clearTimeout(hideTimer);
|
|
317
|
+
if (disposed || suppressFocusTooltip || document.hidden || !visible(card) || panelOpen()
|
|
318
|
+
|| nativeTrigger?.getAttribute('aria-expanded') === 'true') return;
|
|
319
|
+
renderActions();
|
|
204
320
|
tooltip.hidden = false;
|
|
205
321
|
positionTooltip();
|
|
206
322
|
}
|
|
207
323
|
function scheduleTooltip() {
|
|
208
324
|
clearTimeout(hoverTimer);
|
|
325
|
+
clearTimeout(hideTimer);
|
|
209
326
|
hoverTimer = setTimeout(showTooltip, 350);
|
|
210
327
|
}
|
|
211
328
|
|
|
329
|
+
function scheduleHide() {
|
|
330
|
+
clearTimeout(hoverTimer);
|
|
331
|
+
clearTimeout(hideTimer);
|
|
332
|
+
hideTimer = setTimeout(() => {
|
|
333
|
+
if (!tooltip.contains(document.activeElement)) hideTooltip();
|
|
334
|
+
}, 180);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Exact accessible labels cover old builds; the indicator + description covers other locales.
|
|
338
|
+
// Never select the profile menu or a navigation destination merely because it has an SVG.
|
|
339
|
+
function findNativeTrigger(container) {
|
|
340
|
+
return [...(container?.querySelectorAll('button[aria-haspopup="menu"]') ?? [])].find(button =>
|
|
341
|
+
visible(button) && !button.hasAttribute('data-sidebar-destination') && (
|
|
342
|
+
/^(usage|用量|使用量)$/i.test(button.getAttribute('aria-label')?.trim() ?? '')
|
|
343
|
+
|| (button.hasAttribute('aria-description') && button.querySelector('[data-transition-phase]'))
|
|
344
|
+
)) ?? null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const suppressNativeTooltip = event => event.stopPropagation();
|
|
348
|
+
const onBlur = event => {
|
|
349
|
+
if (!tooltip.contains(event.relatedTarget) && !eventTarget?.contains(event.relatedTarget)) hideTooltip();
|
|
350
|
+
};
|
|
351
|
+
const interceptBackground = event => {
|
|
352
|
+
if (forwardingNativeAction || !window.__codexRingsideBackground?.togglePanel) return false;
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
event.stopPropagation();
|
|
355
|
+
return true;
|
|
356
|
+
};
|
|
357
|
+
const onPointerDown = event => {
|
|
358
|
+
// Radix opens before click. Stop the left pointerdown as well as the click so
|
|
359
|
+
// the original menu cannot steal focus or cover the background settings.
|
|
360
|
+
if (event.button === 0 && interceptBackground(event)) eventTarget?.focus();
|
|
361
|
+
};
|
|
362
|
+
const onClick = event => {
|
|
363
|
+
if (interceptBackground(event)) togglePanel();
|
|
364
|
+
else hideTooltip();
|
|
365
|
+
};
|
|
366
|
+
const onContextMenu = event => {
|
|
367
|
+
if (!window.__codexRingsideBackground?.togglePanel) return;
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
event.stopPropagation();
|
|
370
|
+
togglePanel();
|
|
371
|
+
};
|
|
372
|
+
const onKeyDown = event => {
|
|
373
|
+
if (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10')) return onContextMenu(event);
|
|
374
|
+
if (!['Enter', ' ', 'ArrowDown'].includes(event.key)) return;
|
|
375
|
+
if (event.key === 'ArrowDown') {
|
|
376
|
+
if (nativeTrigger && panelOpen()) window.__codexRingsideBackground?.togglePanel(nativeTrigger);
|
|
377
|
+
hideTooltip(); // ArrowDown still reaches the native usage-menu handler.
|
|
378
|
+
} else if (interceptBackground(event)) {
|
|
379
|
+
if (!event.repeat) togglePanel();
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
function bindTarget(target) {
|
|
383
|
+
if (eventTarget === target) return;
|
|
384
|
+
const listeners = { mouseenter: scheduleTooltip, mouseleave: scheduleHide, focus: showTooltip,
|
|
385
|
+
blur: onBlur, pointerdown: onPointerDown, keydown: onKeyDown, click: onClick, contextmenu: onContextMenu,
|
|
386
|
+
pointerover: suppressNativeTooltip, pointermove: suppressNativeTooltip, focusin: suppressNativeTooltip };
|
|
387
|
+
for (const [name, handler] of Object.entries(listeners)) {
|
|
388
|
+
const capture = ['pointerdown', 'keydown', 'click', 'contextmenu'].includes(name);
|
|
389
|
+
eventTarget?.removeEventListener(name, handler, capture);
|
|
390
|
+
target?.addEventListener(name, handler, capture);
|
|
391
|
+
}
|
|
392
|
+
eventTarget = target;
|
|
393
|
+
}
|
|
394
|
+
|
|
212
395
|
function place() {
|
|
213
396
|
if (disposed || !document.body) return;
|
|
214
397
|
if (!style.isConnected) (document.head ?? document.documentElement).appendChild(style);
|
|
@@ -219,6 +402,28 @@ function installCodexRingsideCard() {
|
|
|
219
402
|
rail = nextRail;
|
|
220
403
|
if (rail) resizeObserver.observe(rail);
|
|
221
404
|
}
|
|
405
|
+
const nextTrigger = findNativeTrigger(rail);
|
|
406
|
+
if (nativeTrigger !== nextTrigger) {
|
|
407
|
+
nativeTrigger?.removeAttribute('data-codex-ringside-host');
|
|
408
|
+
nativeTrigger = nextTrigger;
|
|
409
|
+
hideTooltip();
|
|
410
|
+
}
|
|
411
|
+
if (nativeTrigger) {
|
|
412
|
+
if (!nativeTrigger.hasAttribute('data-codex-ringside-host')) nativeTrigger.setAttribute('data-codex-ringside-host', '');
|
|
413
|
+
card.removeAttribute('tabindex');
|
|
414
|
+
card.setAttribute('role', 'group');
|
|
415
|
+
card.removeAttribute('aria-haspopup');
|
|
416
|
+
card.hidden = false;
|
|
417
|
+
if (card.parentElement !== nativeTrigger) nativeTrigger.append(card);
|
|
418
|
+
bindTarget(nativeTrigger);
|
|
419
|
+
if (nativeTrigger.getAttribute('aria-expanded') === 'true') hideTooltip();
|
|
420
|
+
else if (!tooltip.hidden) positionTooltip();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
card.tabIndex = 0;
|
|
424
|
+
card.setAttribute('role', 'button');
|
|
425
|
+
card.setAttribute('aria-haspopup', 'dialog');
|
|
426
|
+
bindTarget(card);
|
|
222
427
|
// Own a sibling just before the rail's footer; never move React-owned nodes.
|
|
223
428
|
const footer = rail && [...rail.children]
|
|
224
429
|
.filter(child => child !== card && visible(child) && getComputedStyle(child).position !== 'absolute')
|
|
@@ -245,20 +450,26 @@ function installCodexRingsideCard() {
|
|
|
245
450
|
if (records.some(record => !ownNode(record.target))) schedulePlacement();
|
|
246
451
|
});
|
|
247
452
|
const resizeObserver = new ResizeObserver(schedulePlacement);
|
|
248
|
-
const
|
|
249
|
-
if (event.key
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
453
|
+
const onEscape = event => {
|
|
454
|
+
if (event.key !== 'Escape' || tooltip.hidden) return;
|
|
455
|
+
if (tooltip.contains(document.activeElement)) {
|
|
456
|
+
suppressFocusTooltip = true;
|
|
457
|
+
eventTarget?.focus();
|
|
458
|
+
suppressFocusTooltip = false;
|
|
459
|
+
}
|
|
460
|
+
hideTooltip();
|
|
461
|
+
};
|
|
462
|
+
const onOutsidePointer = event => {
|
|
463
|
+
if (!tooltip.contains(event.target)) hideTooltip();
|
|
253
464
|
};
|
|
254
465
|
|
|
255
|
-
observer.observe(document.documentElement, { childList: true, subtree: true
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
466
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true,
|
|
467
|
+
attributeFilter: ['aria-expanded', 'aria-label', 'aria-description', 'hidden'] });
|
|
468
|
+
tooltip.addEventListener('mouseenter', () => clearTimeout(hideTimer));
|
|
469
|
+
tooltip.addEventListener('mouseleave', scheduleHide);
|
|
470
|
+
tooltip.addEventListener('focusout', onBlur);
|
|
471
|
+
document.addEventListener('keydown', onEscape);
|
|
472
|
+
document.addEventListener('pointerdown', onOutsidePointer, true);
|
|
262
473
|
window.addEventListener('resize', schedulePlacement);
|
|
263
474
|
document.addEventListener('visibilitychange', hideTooltip);
|
|
264
475
|
|
|
@@ -274,7 +485,10 @@ function installCodexRingsideCard() {
|
|
|
274
485
|
status() {
|
|
275
486
|
return {
|
|
276
487
|
version: VERSION,
|
|
277
|
-
placed: card
|
|
488
|
+
placed: rail?.contains(card) === true && !card.hidden && visible(card),
|
|
489
|
+
replacesNative: nativeTrigger?.contains(card) === true,
|
|
490
|
+
credits: value.credits ?? null,
|
|
491
|
+
plan: value.plan ?? null,
|
|
278
492
|
rings: value.rings,
|
|
279
493
|
message: value.message,
|
|
280
494
|
stale: value.stale ?? false,
|
|
@@ -289,6 +503,10 @@ function installCodexRingsideCard() {
|
|
|
289
503
|
clearTimeout(placementTimer);
|
|
290
504
|
clearInterval(freshnessTimer);
|
|
291
505
|
hideTooltip();
|
|
506
|
+
bindTarget(null);
|
|
507
|
+
nativeTrigger?.removeAttribute('data-codex-ringside-host');
|
|
508
|
+
document.removeEventListener('keydown', onEscape);
|
|
509
|
+
document.removeEventListener('pointerdown', onOutsidePointer, true);
|
|
292
510
|
window.removeEventListener('resize', schedulePlacement);
|
|
293
511
|
document.removeEventListener('visibilitychange', hideTooltip);
|
|
294
512
|
card.remove();
|
package/src/limits.cjs
CHANGED
|
@@ -70,6 +70,8 @@ function mergeSnapshot(current, patch) {
|
|
|
70
70
|
return {
|
|
71
71
|
...base,
|
|
72
72
|
...patch,
|
|
73
|
+
credits: patch.credits === undefined ? base?.credits ?? null
|
|
74
|
+
: isRecord(patch.credits) ? { ...base?.credits, ...patch.credits } : null,
|
|
73
75
|
primary: mergeWindow(base?.primary, patch.primary),
|
|
74
76
|
secondary: mergeWindow(base?.secondary, patch.secondary)
|
|
75
77
|
};
|
|
@@ -81,7 +83,10 @@ function mergeResponse(current, patch) {
|
|
|
81
83
|
if (!isRecord(patch)) return null;
|
|
82
84
|
const changedAccount = hasField(patch, 'accountId') && hasField(current, 'accountId')
|
|
83
85
|
&& patch.accountId !== current.accountId;
|
|
84
|
-
const
|
|
86
|
+
const oldPlan = pickCodexSnapshot(current)?.planType;
|
|
87
|
+
const newPlan = pickCodexSnapshot(patch)?.planType;
|
|
88
|
+
const changedPlan = oldPlan && newPlan && oldPlan !== newPlan;
|
|
89
|
+
const base = changedAccount || changedPlan ? {} : current ?? {};
|
|
85
90
|
const merged = { ...base, ...patch };
|
|
86
91
|
merged.rateLimits = base.rateLimits ?? null;
|
|
87
92
|
if (isRecord(base.rateLimitsByLimitId)) merged.rateLimitsByLimitId = base.rateLimitsByLimitId;
|
|
@@ -122,13 +127,33 @@ function formatUsage(response) {
|
|
|
122
127
|
const percent = remainingPercent(window.usedPercent);
|
|
123
128
|
return { label: labels.short, name: labels.long, percent, tone: tone(percent), resetsAt: formatResetTime(window.resetsAt) };
|
|
124
129
|
});
|
|
130
|
+
const credits = formatCredits(snapshot?.credits);
|
|
125
131
|
return {
|
|
126
132
|
rings,
|
|
127
|
-
|
|
133
|
+
plan: formatPlan(snapshot?.planType),
|
|
134
|
+
credits,
|
|
135
|
+
message: rings.length || credits ? null : '暂时无法读取 Codex 剩余额度',
|
|
128
136
|
note: resetCreditsNote(response)
|
|
129
137
|
};
|
|
130
138
|
}
|
|
131
139
|
|
|
140
|
+
function formatPlan(plan) {
|
|
141
|
+
if (typeof plan !== 'string' || !plan.trim()) return null;
|
|
142
|
+
const labels = { free: 'Free', go: 'Go', plus: 'Plus', pro: 'Pro', team: 'Team', business: 'Business',
|
|
143
|
+
enterprise: 'Enterprise', edu: 'Edu' };
|
|
144
|
+
return Object.hasOwn(labels, plan) ? labels[plan] : plan;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatCredits(credits) {
|
|
148
|
+
if (!isRecord(credits)) return null;
|
|
149
|
+
if (credits.unlimited === true) return { unlimited: true, balance: null, label: '不限额' };
|
|
150
|
+
const raw = credits.balance;
|
|
151
|
+
if (typeof raw !== 'number' && (typeof raw !== 'string' || !raw.trim())) return null;
|
|
152
|
+
const balance = Number(raw);
|
|
153
|
+
if (!Number.isFinite(balance) || balance < 0) return null;
|
|
154
|
+
return { unlimited: false, balance, label: new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 0 }).format(balance) };
|
|
155
|
+
}
|
|
156
|
+
|
|
132
157
|
function resetCreditsNote(response) {
|
|
133
158
|
const count = Number(response?.rateLimitResetCredits?.availableCount);
|
|
134
159
|
return Number.isFinite(count) && count > 0 ? `重置卡:${Math.floor(count)} 张可用` : null;
|
|
@@ -139,6 +164,9 @@ function unavailable(value, message = '暂时无法读取 Codex 剩余额度,
|
|
|
139
164
|
...value,
|
|
140
165
|
message,
|
|
141
166
|
stale: true,
|
|
167
|
+
credits: null,
|
|
168
|
+
plan: null,
|
|
169
|
+
note: null,
|
|
142
170
|
rings: (value?.rings ?? []).map(ring => ({ ...ring, percent: null, tone: 'muted', resetsAt: null }))
|
|
143
171
|
};
|
|
144
172
|
}
|