diff --git a/README.md b/README.md index 18e10f1..e601455 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - 支持在同一总览页查看多个 Codex 账号或工作区(包括同一邮箱的个人订阅与 Team 工作区) - 展示 Codex 账户、套餐、剩余额度和下次重置时间 - 展示当前重置周期 Token 合计、本周期单日峰值及每日 Token 趋势 -- 登录后有可用重置卡时展示卡片数量和到期时间 +- 有可用重置卡时展示卡片数量和到期时间 - 在本地 SQLite 中保留历史用量和限额快照 - 在限额重置前、重置后发送 Telegram 或邮件提醒 - 通过 Telegram 菜单查询当前用量、重置时间、历史概览和账户信息 diff --git a/backend/CONTRACT.md b/backend/CONTRACT.md index b2e915b..814a16a 100644 --- a/backend/CONTRACT.md +++ b/backend/CONTRACT.md @@ -41,7 +41,7 @@ ``` 时间字段为 Unix 秒。`currentCycle` 是按当前仍有效的最长限额窗口计算的当前重置周期;`totalTokens` 由 `account/usage/read` 的每日 Token bucket 汇总,受每日粒度影响,无法精确切分周期起止日期内的单日数据。`summary.peakDailyTokens` 是同一当前重置周期内每日 Token bucket 的最大值,不再直接采用 app-server 未限定口径的峰值摘要;没有有效窗口或周期内没有每日数据时为 `null`。app-server 未提供的其他摘要字段可以是 `null`;列表应返回数组而非 `null`。 -`resetCredits` 仅在 app-server 返回可用重置卡且 `availableCount > 0` 时出现;`expiresAt` 是已返回卡片的去重到期时间列表,服务端未提供卡片详情时可以为空。匿名公开 Dashboard 不返回该字段。 +`resetCredits` 仅在 app-server 返回可用重置卡且 `availableCount > 0` 时出现;`expiresAt` 是已返回卡片的去重到期时间列表,服务端未提供卡片详情时可以为空。公开账号的匿名 Dashboard 也会返回该字段;邮箱、认证方式和内部错误字段仍会隐藏。 ## 3. 系统、初始化与会话 @@ -66,7 +66,7 @@ | `POST /api/v1/accounts/{id}/login/device` | 启动并初始化 app-server,调用 `account/login/start` 的 `chatgptDeviceCode` 流程;返回含 `verificationUrl`、`userCode` 和 `loginId` 的结果。 | | `POST /api/v1/accounts/{id}/logout` | 调用 `account/logout` 并将连接状态置为 false;返回 `200 {ok:true}`。 | | `POST /api/v1/accounts/{id}/sync` | 同步指定账号;成功 `200 {ok:true}`,上游失败 502。 | -| `GET`/`HEAD /api/v1/dashboard?accountId={id}` | 初始化后匿名可读公开账号,返回内存中的 `Dashboard`;匿名访问未公开账号返回 404,匿名响应隐藏邮箱、认证方式、重置卡和内部错误字段。登录后可读取全部账号。省略或无效的零值 ID 使用账号 1,前端使用 `GET`。 | +| `GET`/`HEAD /api/v1/dashboard?accountId={id}` | 初始化后匿名可读公开账号,返回内存中的 `Dashboard`;匿名访问未公开账号返回 404,匿名响应隐藏邮箱、认证方式和内部错误字段,但保留重置卡信息。登录后可读取全部账号。省略或无效的零值 ID 使用账号 1,前端使用 `GET`。 | | `任意非读方法 /api/v1/dashboard?accountId={id}` | 要求 session;非 `GET`/`HEAD` 还需来源头。保持兼容的读取行为,省略或无效的零值 ID 使用账号 1。 | | `POST /api/v1/sync?accountId={id}` | 旧兼容入口,同步指定账号;省略或零值 ID 使用账号 1。 | diff --git a/backend/internal/app/api.go b/backend/internal/app/api.go index 3e042f9..4d895c2 100644 --- a/backend/internal/app/api.go +++ b/backend/internal/app/api.go @@ -230,7 +230,6 @@ func (a *App) dashboardAPI(w http.ResponseWriter, r *http.Request) { func publicDashboard(d Dashboard) Dashboard { d.Account.Email = nil d.Account.AuthMode = nil - d.ResetCredits = nil d.LastError = "" return d } diff --git a/backend/internal/app/runtime_test.go b/backend/internal/app/runtime_test.go index eeddd73..632450f 100644 --- a/backend/internal/app/runtime_test.go +++ b/backend/internal/app/runtime_test.go @@ -134,8 +134,8 @@ func TestAnonymousOverviewIsReadOnly(t *testing.T) { if publicDashboardBody.Account.Email != nil || publicDashboardBody.Account.AuthMode != nil { t.Fatalf("anonymous dashboard account = %#v; identity fields were not redacted", publicDashboardBody.Account) } - if publicDashboardBody.ResetCredits != nil { - t.Fatalf("anonymous dashboard reset credits = %#v; entitlement data was not redacted", publicDashboardBody.ResetCredits) + if publicDashboardBody.ResetCredits == nil || publicDashboardBody.ResetCredits.AvailableCount != 2 { + t.Fatalf("anonymous dashboard reset credits = %#v; want two credits", publicDashboardBody.ResetCredits) } for _, path := range []string{"/api/v1/settings/general", "/api/v1/accounts", "/api/v1/accounts/1/sync"} { diff --git a/docs/frontend/application.md b/docs/frontend/application.md index eb9ace3..dd832d0 100644 --- a/docs/frontend/application.md +++ b/docs/frontend/application.md @@ -18,7 +18,7 @@ API 类型精确区分后端 `null` 与 optional,并为秘密设置拆分读 ## 总览与设置 -总览显示账户连接、套餐、每个限额窗口的剩余百分比和重置时间、当前重置周期 Token 合计、本周期单日峰值、其他摘要及每日 Token 图;登录后账号有可用重置卡时,详情额外显示卡片数量和到期时间。当前周期 Token 与本周期单日峰值都由后端按 app-server 的有效最长限额窗口和每日 bucket 汇总,不包含周期外或未来日期;详情会显示周期起止时间和“按每日数据汇总”提示。`usedPercent` 展示前限制到 0–100,但原始数据语义不得在 API 类型层改写。缺失摘要显示“暂无”,缺失限额显示明确空状态。匿名公开 Dashboard 不显示重置卡信息。 +总览显示账户连接、套餐、每个限额窗口的剩余百分比和重置时间、当前重置周期 Token 合计、本周期单日峰值、其他摘要及每日 Token 图;账号有可用重置卡时,详情额外显示卡片数量和到期时间,公开账号的匿名总览也会显示。当前周期 Token 与本周期单日峰值都由后端按 app-server 的有效最长限额窗口和每日 bucket 汇总,不包含周期外或未来日期;详情会显示周期起止时间和“按每日数据汇总”提示。`usedPercent` 展示前限制到 0–100,但原始数据语义不得在 API 类型层改写。缺失摘要显示“暂无”,缺失限额显示明确空状态。匿名公开 Dashboard 仍隐藏邮箱、认证方式和内部错误。 设置页四个 tab 全部保持挂载,以保留未提交表单状态;非活动 panel 使用 `aria-hidden` 和 `inert` 隔离。tab 支持方向键、Home 和 End,程序化切换不得改变页面滚动和布局。账号删除必须保留不可撤销确认,并清理正在显示的设备码和本地账号选择。 diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index f440735..9b49caa 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -715,21 +715,19 @@ function AccountDashboardDetails({ value={dashboard.stale ? "数据可能已过期" : "数据已更新"} /> - {!publicView && - dashboard.resetCredits && - dashboard.resetCredits.availableCount > 0 && ( -
- -
- 重置卡 - 可用 {dashboard.resetCredits.availableCount} 张 -
-
- 到期时间 - {formatResetCreditExpiry(dashboard.resetCredits.expiresAt)} -
+ {dashboard.resetCredits && dashboard.resetCredits.availableCount > 0 && ( +
+ +
+ 重置卡 + 可用 {dashboard.resetCredits.availableCount} 张
- )} +
+ 到期时间 + {formatResetCreditExpiry(dashboard.resetCredits.expiresAt)} +
+
+ )}
{dashboard.limits.map((limit, index) => ( { connected: true, }, limits: [], + resetCredits: { + availableCount: 2, + expiresAt: [1_784_246_400], + }, summary: {}, usage: [], fetchedAt: 1_900_000_000, @@ -235,6 +239,8 @@ test("shows a read-only overview before login", async ({ page }) => { const account = page.locator('[data-account-id="1"]'); await account.getByRole("button", { name: "展开默认账号详情" }).click(); await expect(account.getByText("登录后查看")).toBeVisible(); + await expect(account.getByTestId("reset-credits")).toContainText("可用 2 张"); + await expect(account.getByTestId("reset-credits")).toContainText("到期时间"); await expect(account.locator(".account-detail .refresh")).toHaveCount(0); await page.goto("/settings");