feat: show reset credits publicly
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
- 支持在同一总览页查看多个 Codex 账号或工作区(包括同一邮箱的个人订阅与 Team 工作区)
|
- 支持在同一总览页查看多个 Codex 账号或工作区(包括同一邮箱的个人订阅与 Team 工作区)
|
||||||
- 展示 Codex 账户、套餐、剩余额度和下次重置时间
|
- 展示 Codex 账户、套餐、剩余额度和下次重置时间
|
||||||
- 展示当前重置周期 Token 合计、本周期单日峰值及每日 Token 趋势
|
- 展示当前重置周期 Token 合计、本周期单日峰值及每日 Token 趋势
|
||||||
- 登录后有可用重置卡时展示卡片数量和到期时间
|
- 有可用重置卡时展示卡片数量和到期时间
|
||||||
- 在本地 SQLite 中保留历史用量和限额快照
|
- 在本地 SQLite 中保留历史用量和限额快照
|
||||||
- 在限额重置前、重置后发送 Telegram 或邮件提醒
|
- 在限额重置前、重置后发送 Telegram 或邮件提醒
|
||||||
- 通过 Telegram 菜单查询当前用量、重置时间、历史概览和账户信息
|
- 通过 Telegram 菜单查询当前用量、重置时间、历史概览和账户信息
|
||||||
|
|||||||
+2
-2
@@ -41,7 +41,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
时间字段为 Unix 秒。`currentCycle` 是按当前仍有效的最长限额窗口计算的当前重置周期;`totalTokens` 由 `account/usage/read` 的每日 Token bucket 汇总,受每日粒度影响,无法精确切分周期起止日期内的单日数据。`summary.peakDailyTokens` 是同一当前重置周期内每日 Token bucket 的最大值,不再直接采用 app-server 未限定口径的峰值摘要;没有有效窗口或周期内没有每日数据时为 `null`。app-server 未提供的其他摘要字段可以是 `null`;列表应返回数组而非 `null`。
|
时间字段为 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. 系统、初始化与会话
|
## 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}/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}/logout` | 调用 `account/logout` 并将连接状态置为 false;返回 `200 {ok:true}`。 |
|
||||||
| `POST /api/v1/accounts/{id}/sync` | 同步指定账号;成功 `200 {ok:true}`,上游失败 502。 |
|
| `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。 |
|
| `任意非读方法 /api/v1/dashboard?accountId={id}` | 要求 session;非 `GET`/`HEAD` 还需来源头。保持兼容的读取行为,省略或无效的零值 ID 使用账号 1。 |
|
||||||
| `POST /api/v1/sync?accountId={id}` | 旧兼容入口,同步指定账号;省略或零值 ID 使用账号 1。 |
|
| `POST /api/v1/sync?accountId={id}` | 旧兼容入口,同步指定账号;省略或零值 ID 使用账号 1。 |
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,6 @@ func (a *App) dashboardAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
func publicDashboard(d Dashboard) Dashboard {
|
func publicDashboard(d Dashboard) Dashboard {
|
||||||
d.Account.Email = nil
|
d.Account.Email = nil
|
||||||
d.Account.AuthMode = nil
|
d.Account.AuthMode = nil
|
||||||
d.ResetCredits = nil
|
|
||||||
d.LastError = ""
|
d.LastError = ""
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ func TestAnonymousOverviewIsReadOnly(t *testing.T) {
|
|||||||
if publicDashboardBody.Account.Email != nil || publicDashboardBody.Account.AuthMode != nil {
|
if publicDashboardBody.Account.Email != nil || publicDashboardBody.Account.AuthMode != nil {
|
||||||
t.Fatalf("anonymous dashboard account = %#v; identity fields were not redacted", publicDashboardBody.Account)
|
t.Fatalf("anonymous dashboard account = %#v; identity fields were not redacted", publicDashboardBody.Account)
|
||||||
}
|
}
|
||||||
if publicDashboardBody.ResetCredits != nil {
|
if publicDashboardBody.ResetCredits == nil || publicDashboardBody.ResetCredits.AvailableCount != 2 {
|
||||||
t.Fatalf("anonymous dashboard reset credits = %#v; entitlement data was not redacted", publicDashboardBody.ResetCredits)
|
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"} {
|
for _, path := range []string{"/api/v1/settings/general", "/api/v1/accounts", "/api/v1/accounts/1/sync"} {
|
||||||
|
|||||||
@@ -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,程序化切换不得改变页面滚动和布局。账号删除必须保留不可撤销确认,并清理正在显示的设备码和本地账号选择。
|
设置页四个 tab 全部保持挂载,以保留未提交表单状态;非活动 panel 使用 `aria-hidden` 和 `inert` 隔离。tab 支持方向键、Home 和 End,程序化切换不得改变页面滚动和布局。账号删除必须保留不可撤销确认,并清理正在显示的设备码和本地账号选择。
|
||||||
|
|
||||||
|
|||||||
+12
-14
@@ -715,21 +715,19 @@ function AccountDashboardDetails({
|
|||||||
value={dashboard.stale ? "数据可能已过期" : "数据已更新"}
|
value={dashboard.stale ? "数据可能已过期" : "数据已更新"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!publicView &&
|
{dashboard.resetCredits && dashboard.resetCredits.availableCount > 0 && (
|
||||||
dashboard.resetCredits &&
|
<div className="reset-credits" data-testid="reset-credits">
|
||||||
dashboard.resetCredits.availableCount > 0 && (
|
<Ticket />
|
||||||
<div className="reset-credits" data-testid="reset-credits">
|
<div className="reset-credits-main">
|
||||||
<Ticket />
|
<small>重置卡</small>
|
||||||
<div className="reset-credits-main">
|
<b>可用 {dashboard.resetCredits.availableCount} 张</b>
|
||||||
<small>重置卡</small>
|
|
||||||
<b>可用 {dashboard.resetCredits.availableCount} 张</b>
|
|
||||||
</div>
|
|
||||||
<div className="reset-credits-expiry">
|
|
||||||
<small>到期时间</small>
|
|
||||||
<b>{formatResetCreditExpiry(dashboard.resetCredits.expiresAt)}</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="reset-credits-expiry">
|
||||||
|
<small>到期时间</small>
|
||||||
|
<b>{formatResetCreditExpiry(dashboard.resetCredits.expiresAt)}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="limits">
|
<div className="limits">
|
||||||
{dashboard.limits.map((limit, index) => (
|
{dashboard.limits.map((limit, index) => (
|
||||||
<LimitCard
|
<LimitCard
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ test("shows a read-only overview before login", async ({ page }) => {
|
|||||||
connected: true,
|
connected: true,
|
||||||
},
|
},
|
||||||
limits: [],
|
limits: [],
|
||||||
|
resetCredits: {
|
||||||
|
availableCount: 2,
|
||||||
|
expiresAt: [1_784_246_400],
|
||||||
|
},
|
||||||
summary: {},
|
summary: {},
|
||||||
usage: [],
|
usage: [],
|
||||||
fetchedAt: 1_900_000_000,
|
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"]');
|
const account = page.locator('[data-account-id="1"]');
|
||||||
await account.getByRole("button", { name: "展开默认账号详情" }).click();
|
await account.getByRole("button", { name: "展开默认账号详情" }).click();
|
||||||
await expect(account.getByText("登录后查看")).toBeVisible();
|
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 expect(account.locator(".account-detail .refresh")).toHaveCount(0);
|
||||||
|
|
||||||
await page.goto("/settings");
|
await page.goto("/settings");
|
||||||
|
|||||||
Reference in New Issue
Block a user