feat: show current-cycle daily token peak
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
- 支持在同一总览页查看多个 Codex 账号或工作区(包括同一邮箱的个人订阅与 Team 工作区)
|
- 支持在同一总览页查看多个 Codex 账号或工作区(包括同一邮箱的个人订阅与 Team 工作区)
|
||||||
- 展示 Codex 账户、套餐、剩余额度和下次重置时间
|
- 展示 Codex 账户、套餐、剩余额度和下次重置时间
|
||||||
- 展示当前重置周期 Token 合计、单日峰值、连续使用天数及每日 Token 趋势
|
- 展示当前重置周期 Token 合计、本周期单日峰值、连续使用天数及每日 Token 趋势
|
||||||
- 在本地 SQLite 中保留历史用量和限额快照
|
- 在本地 SQLite 中保留历史用量和限额快照
|
||||||
- 在限额重置前、重置后发送 Telegram 或邮件提醒
|
- 在限额重置前、重置后发送 Telegram 或邮件提醒
|
||||||
- 通过 Telegram 菜单查询当前用量、重置时间、历史概览和账户信息
|
- 通过 Telegram 菜单查询当前用量、重置时间、历史概览和账户信息
|
||||||
@@ -171,7 +171,7 @@ Codex Helper 展示的是 Codex app-server 实际返回的数据。当前接口
|
|||||||
|
|
||||||
- ChatGPT/Codex 账户和套餐类型
|
- ChatGPT/Codex 账户和套餐类型
|
||||||
- 限额窗口使用百分比、窗口长度和重置时间
|
- 限额窗口使用百分比、窗口长度和重置时间
|
||||||
- 当前重置周期 Token 合计、单日峰值、连续使用天数、最长任务时长等摘要
|
- 当前重置周期 Token 合计、本周期单日峰值、连续使用天数、最长任务时长等摘要
|
||||||
- 每日总 Token 桶
|
- 每日总 Token 桶
|
||||||
|
|
||||||
当前总览的 Token 合计按 app-server 返回的当前有效最长限额窗口汇总每日 Token bucket,通常对应 secondary 周窗口;周期边界所在日期按整日统计。系统不根据 Token 推算 Credits、美元价值或订阅价格。部分摘要或每日数据也可能因账户或服务端暂未返回而显示为“暂无”。根据 OpenAI 官方文档,`account/usage/read` 需要 Codex 服务支持的身份认证;仅 API Key 或 Bedrock 登录不能读取这些 ChatGPT 用量数据。
|
当前总览的 Token 合计按 app-server 返回的当前有效最长限额窗口汇总每日 Token bucket,通常对应 secondary 周窗口;周期边界所在日期按整日统计。系统不根据 Token 推算 Credits、美元价值或订阅价格。部分摘要或每日数据也可能因账户或服务端暂未返回而显示为“暂无”。根据 OpenAI 官方文档,`account/usage/read` 需要 Codex 服务支持的身份认证;仅 API Key 或 Bedrock 登录不能读取这些 ChatGPT 用量数据。
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@
|
|||||||
fetchedAt, stale, lastError?}
|
fetchedAt, stale, lastError?}
|
||||||
```
|
```
|
||||||
|
|
||||||
时间字段为 Unix 秒。`currentCycle` 是按当前仍有效的最长限额窗口计算的当前重置周期;`totalTokens` 由 `account/usage/read` 的每日 Token bucket 汇总,受每日粒度影响,无法精确切分周期起止日期内的单日数据。没有有效窗口或用量接口失败时省略该字段。app-server 未提供的摘要字段可以是 `null`;列表应返回数组而非 `null`。
|
时间字段为 Unix 秒。`currentCycle` 是按当前仍有效的最长限额窗口计算的当前重置周期;`totalTokens` 由 `account/usage/read` 的每日 Token bucket 汇总,受每日粒度影响,无法精确切分周期起止日期内的单日数据。`summary.peakDailyTokens` 是同一当前重置周期内每日 Token bucket 的最大值,不再直接采用 app-server 未限定口径的峰值摘要;没有有效窗口或周期内没有每日数据时为 `null`。app-server 未提供的其他摘要字段可以是 `null`;列表应返回数组而非 `null`。
|
||||||
|
|
||||||
## 3. 系统、初始化与会话
|
## 3. 系统、初始化与会话
|
||||||
|
|
||||||
|
|||||||
@@ -533,6 +533,7 @@ func (a *App) syncAccount(ctx context.Context, id int64) error {
|
|||||||
_, _ = a.store.DB.Exec("INSERT INTO daily_usage(account_id,date,total_tokens,fetched_at) VALUES(?,?,?,?) ON CONFLICT(account_id,date) DO UPDATE SET total_tokens=excluded.total_tokens,fetched_at=excluded.fetched_at", id, x.StartDate, x.Tokens, d.FetchedAt)
|
_, _ = a.store.DB.Exec("INSERT INTO daily_usage(account_id,date,total_tokens,fetched_at) VALUES(?,?,?,?) ON CONFLICT(account_id,date) DO UPDATE SET total_tokens=excluded.total_tokens,fetched_at=excluded.fetched_at", id, x.StartDate, x.Tokens, d.FetchedAt)
|
||||||
}
|
}
|
||||||
d.CurrentCycle = currentTokenCycle(d.Limits, d.Usage, d.FetchedAt)
|
d.CurrentCycle = currentTokenCycle(d.Limits, d.Usage, d.FetchedAt)
|
||||||
|
d.Summary.PeakDailyTokens = peakDailyTokensForCycle(d.CurrentCycle, d.Usage, d.FetchedAt)
|
||||||
}
|
}
|
||||||
resetDetected, e := a.storeLimitSnapshots(d)
|
resetDetected, e := a.storeLimitSnapshots(d)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
@@ -642,6 +643,29 @@ func currentTokenCycle(limits []LimitBucket, usage []UsagePoint, fetchedAt int64
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func peakDailyTokensForCycle(cycle *TokenCycle, usage []UsagePoint, fetchedAt int64) *int64 {
|
||||||
|
if cycle == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
startDate := time.Unix(cycle.StartedAt, 0).UTC().Format("2006-01-02")
|
||||||
|
endDate := time.Unix(fetchedAt, 0).UTC().Format("2006-01-02")
|
||||||
|
var peak int64
|
||||||
|
found := false
|
||||||
|
for _, point := range usage {
|
||||||
|
if _, err := time.Parse("2006-01-02", point.Date); err != nil || point.Date < startDate || point.Date > endDate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !found || point.TotalTokens > peak {
|
||||||
|
peak = point.TotalTokens
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &peak
|
||||||
|
}
|
||||||
|
|
||||||
func betterTokenCycleLimit(candidate, current LimitBucket) bool {
|
func betterTokenCycleLimit(candidate, current LimitBucket) bool {
|
||||||
if candidate.WindowDurationMinutes != current.WindowDurationMinutes {
|
if candidate.WindowDurationMinutes != current.WindowDurationMinutes {
|
||||||
return candidate.WindowDurationMinutes > current.WindowDurationMinutes
|
return candidate.WindowDurationMinutes > current.WindowDurationMinutes
|
||||||
|
|||||||
@@ -331,6 +331,24 @@ func TestCurrentTokenCycleRequiresAValidFutureResetWindow(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPeakDailyTokensUsesOnlyTheCurrentTokenCycle(t *testing.T) {
|
||||||
|
now := time.Date(2026, time.August, 14, 12, 0, 0, 0, time.UTC)
|
||||||
|
cycle := &TokenCycle{
|
||||||
|
StartedAt: time.Date(2026, time.August, 8, 0, 0, 0, 0, time.UTC).Unix(),
|
||||||
|
ResetsAt: time.Date(2026, time.August, 15, 0, 0, 0, 0, time.UTC).Unix(),
|
||||||
|
}
|
||||||
|
peak := peakDailyTokensForCycle(cycle, []UsagePoint{
|
||||||
|
{Date: "2026-08-07", TotalTokens: 900},
|
||||||
|
{Date: "2026-08-08", TotalTokens: 100},
|
||||||
|
{Date: "2026-08-10", TotalTokens: 200},
|
||||||
|
{Date: "2026-08-14", TotalTokens: 300},
|
||||||
|
{Date: "2026-08-15", TotalTokens: 800},
|
||||||
|
}, now.Unix())
|
||||||
|
if peak == nil || *peak != 300 {
|
||||||
|
t.Fatalf("cycle peak = %v; want 300", peak)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFlattenLimitReadsAppServerWindowDuration(t *testing.T) {
|
func TestFlattenLimitReadsAppServerWindowDuration(t *testing.T) {
|
||||||
var limit rawLimit
|
var limit rawLimit
|
||||||
if err := json.Unmarshal([]byte(`{
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ API 类型精确区分后端 `null` 与 optional,并为秘密设置拆分读
|
|||||||
|
|
||||||
## 总览与设置
|
## 总览与设置
|
||||||
|
|
||||||
总览显示账户连接、套餐、每个限额窗口的剩余百分比和重置时间、当前重置周期 Token 合计、其他摘要及每日 Token 图。当前周期 Token 由后端按 app-server 的有效最长限额窗口和每日 bucket 汇总,不包含周期外或未来日期;详情会显示周期起止时间和“按每日数据汇总”提示。`usedPercent` 展示前限制到 0–100,但原始数据语义不得在 API 类型层改写。缺失摘要显示“暂无”,缺失限额显示明确空状态。
|
总览显示账户连接、套餐、每个限额窗口的剩余百分比和重置时间、当前重置周期 Token 合计、本周期单日峰值、其他摘要及每日 Token 图。当前周期 Token 与本周期单日峰值都由后端按 app-server 的有效最长限额窗口和每日 bucket 汇总,不包含周期外或未来日期;详情会显示周期起止时间和“按每日数据汇总”提示。`usedPercent` 展示前限制到 0–100,但原始数据语义不得在 API 类型层改写。缺失摘要显示“暂无”,缺失限额显示明确空状态。
|
||||||
|
|
||||||
设置页四个 tab 全部保持挂载,以保留未提交表单状态;非活动 panel 使用 `aria-hidden` 和 `inert` 隔离。tab 支持方向键、Home 和 End,程序化切换不得改变页面滚动和布局。账号删除必须保留不可撤销确认,并清理正在显示的设备码和本地账号选择。
|
设置页四个 tab 全部保持挂载,以保留未提交表单状态;非活动 panel 使用 `aria-hidden` 和 `inert` 隔离。tab 支持方向键、Home 和 End,程序化切换不得改变页面滚动和布局。账号删除必须保留不可撤销确认,并清理正在显示的设备码和本地账号选择。
|
||||||
|
|
||||||
|
|||||||
@@ -843,7 +843,7 @@ function AccountDashboardDetails({
|
|||||||
/>
|
/>
|
||||||
<Stat
|
<Stat
|
||||||
icon={<TrendingUp />}
|
icon={<TrendingUp />}
|
||||||
label="单日峰值"
|
label="本周期单日峰值"
|
||||||
v={num(dashboard.summary.peakDailyTokens)}
|
v={num(dashboard.summary.peakDailyTokens)}
|
||||||
/>
|
/>
|
||||||
<Stat
|
<Stat
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ test("shows every account as a collapsible summary on the overview", async ({
|
|||||||
first.getByRole("heading", { name: "每日 Token 趋势" }),
|
first.getByRole("heading", { name: "每日 Token 趋势" }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(first.getByText("本周期 Tokens")).toBeVisible();
|
await expect(first.getByText("本周期 Tokens")).toBeVisible();
|
||||||
|
await expect(first.getByText("本周期单日峰值")).toBeVisible();
|
||||||
await expect(first.getByText("123.5K", { exact: true })).toBeVisible();
|
await expect(first.getByText("123.5K", { exact: true })).toBeVisible();
|
||||||
await expect(first.getByText("按每日数据汇总")).toBeVisible();
|
await expect(first.getByText("按每日数据汇总")).toBeVisible();
|
||||||
await expect(page.locator(".account-detail")).toHaveCount(1);
|
await expect(page.locator(".account-detail")).toHaveCount(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user