Skip to content

Commit 39a4881

Browse files
committed
refactor(usage): align token-plan with --output convention and tolerant quota reading
1 parent 4d84af6 commit 39a4881

7 files changed

Lines changed: 198 additions & 207 deletions

File tree

packages/commands/src/commands/usage/shared.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ export function formatDate(ts: number): string {
2424
return `${year}-${month}-${day}`;
2525
}
2626

27+
export function formatDateTime(ts: number): string {
28+
const date = new Date(ts);
29+
const hour = String(date.getHours()).padStart(2, "0");
30+
const minute = String(date.getMinutes()).padStart(2, "0");
31+
const second = String(date.getSeconds()).padStart(2, "0");
32+
return `${formatDate(ts)} ${hour}:${minute}:${second}`;
33+
}
34+
2735
export function requireWorkspaceId(settings: Settings, binName: string): string {
2836
if (settings.workspaceId) return settings.workspaceId;
2937

packages/commands/src/commands/usage/token-plan.ts

Lines changed: 63 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { BailianError, ExitCode, defineCommand, unwrapResponse } from "bailian-cli-core";
2-
import { ansi, displayWidth, emitResult, type TextStyle } from "bailian-cli-runtime";
1+
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
2+
import {
3+
ansi,
4+
displayWidth,
5+
emitResult,
6+
type AnsiStyles,
7+
type TextStyle,
8+
} from "bailian-cli-runtime";
9+
import { formatDateTime } from "./shared.ts";
310

411
const TOKEN_PLAN_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";
512
const BOX_WIDTH = 76;
@@ -12,50 +19,36 @@ interface TokenPlanUsage {
1219
per1WeekResetTime?: number;
1320
}
1421

15-
function readUsage(result: unknown): TokenPlanUsage {
16-
const response = unwrapResponse(result as Record<string, unknown>);
17-
const usage = {
18-
per5HourPercentage: response.per5HourPercentage,
19-
per5HourResetTime: response.per5HourResetTime,
20-
per1WeekPercentage: response.per1WeekPercentage,
21-
per1WeekResetTime: response.per1WeekResetTime,
22-
};
23-
24-
const quotas = [
25-
[usage.per5HourPercentage, usage.per5HourResetTime],
26-
[usage.per1WeekPercentage, usage.per1WeekResetTime],
27-
];
28-
const hasValidQuotas = quotas.every(
29-
([percentage, resetTime]) =>
30-
(percentage === undefined && resetTime === undefined) ||
31-
(typeof percentage === "number" &&
32-
Number.isFinite(percentage) &&
33-
((percentage === 0 && resetTime === undefined) ||
34-
(typeof resetTime === "number" && Number.isFinite(resetTime)))),
35-
);
22+
interface QuotaWindow {
23+
percentage?: number;
24+
resetTime?: number;
25+
}
3626

37-
if (!hasValidQuotas) {
38-
throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL);
39-
}
27+
/** Accept only finite numbers; anything else counts as absent (possibly unlimited). */
28+
function readNumber(value: unknown): number | undefined {
29+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
30+
}
4031

41-
return usage as TokenPlanUsage;
32+
function readUsage(result: unknown): TokenPlanUsage {
33+
const response = unwrapResponse(result as Record<string, unknown>);
34+
const usage: TokenPlanUsage = {};
35+
36+
const per5HourPercentage = readNumber(response.per5HourPercentage);
37+
if (per5HourPercentage !== undefined) usage.per5HourPercentage = per5HourPercentage;
38+
const per5HourResetTime = readNumber(response.per5HourResetTime);
39+
if (per5HourResetTime !== undefined) usage.per5HourResetTime = per5HourResetTime;
40+
const per1WeekPercentage = readNumber(response.per1WeekPercentage);
41+
if (per1WeekPercentage !== undefined) usage.per1WeekPercentage = per1WeekPercentage;
42+
const per1WeekResetTime = readNumber(response.per1WeekResetTime);
43+
if (per1WeekResetTime !== undefined) usage.per1WeekResetTime = per1WeekResetTime;
44+
45+
return usage;
4246
}
4347

4448
function formatPercentage(ratio: number): string {
4549
return `${(ratio * 100).toFixed(2)}%`;
4650
}
4751

48-
function formatDateTime(timestamp: number): string {
49-
const date = new Date(timestamp);
50-
const year = date.getFullYear();
51-
const month = String(date.getMonth() + 1).padStart(2, "0");
52-
const day = String(date.getDate()).padStart(2, "0");
53-
const hour = String(date.getHours()).padStart(2, "0");
54-
const minute = String(date.getMinutes()).padStart(2, "0");
55-
const second = String(date.getSeconds()).padStart(2, "0");
56-
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
57-
}
58-
5952
function formatRemainingTime(resetTime: number, now: number): string {
6053
const remainingMs = Math.max(0, resetTime - now);
6154
const totalMinutes = Math.floor(remainingMs / 60_000);
@@ -77,102 +70,74 @@ function progressBar(ratio: number): string {
7770
return `[${"█".repeat(filled)}${"░".repeat(PROGRESS_WIDTH - filled)}]`;
7871
}
7972

80-
function progressStyle(
81-
percentage: number,
82-
green: TextStyle,
83-
yellow: TextStyle,
84-
red: TextStyle,
85-
): TextStyle {
86-
if (percentage >= 0.9) return red;
87-
if (percentage >= 0.75) return yellow;
88-
return green;
73+
function progressStyle(percentage: number, color: AnsiStyles): TextStyle {
74+
if (percentage >= 0.9) return color.red;
75+
if (percentage >= 0.75) return color.yellow;
76+
return color.green;
8977
}
9078

9179
function printView(usage: TokenPlanUsage, generatedAt: number): void {
9280
const color = ansi(process.stdout);
93-
const writeLine = (content = "", visibleContent = content) => {
94-
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${visibleContent}`));
95-
process.stdout.write(`│ ${content}${" ".repeat(padding)}│\n`);
81+
const writeLine = (text = "", style?: TextStyle) => {
82+
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${text}`));
83+
process.stdout.write(`│ ${style ? style(text) : text}${" ".repeat(padding)}│\n`);
9684
};
97-
const writeQuota = (
98-
label: string,
99-
unlimitedMessage: string,
100-
percentage: number | undefined,
101-
resetTime: number | undefined,
102-
) => {
103-
writeLine(color.bold(label), label);
104-
if (percentage === undefined) {
105-
writeLine(color.dim(unlimitedMessage), unlimitedMessage);
85+
const writeQuota = (label: string, unlimitedMessage: string, window: QuotaWindow) => {
86+
writeLine(label, color.bold);
87+
if (window.percentage === undefined) {
88+
writeLine(unlimitedMessage, color.dim);
10689
return;
10790
}
10891

109-
const percentageText = formatPercentage(percentage);
110-
const bar = progressBar(percentage);
111-
const style = progressStyle(percentage, color.green, color.yellow, color.red);
112-
writeLine(`${percentageText} used ${style(bar)}`, `${percentageText} used ${bar}`);
113-
if (resetTime === undefined) {
114-
writeLine(
115-
color.dim("Resets: not applicable (no usage yet)"),
116-
"Resets: not applicable (no usage yet)",
117-
);
92+
const percentageText = formatPercentage(window.percentage);
93+
const bar = progressBar(window.percentage);
94+
writeLine(`${percentageText} used ${bar}`, progressStyle(window.percentage, color));
95+
if (window.resetTime === undefined) {
96+
writeLine("Resets: not applicable (no usage yet)", color.dim);
11897
return;
11998
}
12099

121-
const resetText = `Resets: ${formatDateTime(resetTime)} (in ${formatRemainingTime(resetTime, generatedAt)})`;
122-
writeLine(color.dim(resetText), resetText);
100+
const resetText = `Resets: ${formatDateTime(window.resetTime)} (in ${formatRemainingTime(window.resetTime, generatedAt)})`;
101+
writeLine(resetText, color.dim);
123102
};
124103

125104
process.stdout.write(`┌${"─".repeat(BOX_WIDTH)}┐\n`);
126-
writeLine(color.cyan("Token Plan Usage"), "Token Plan Usage");
127-
const generatedAtText = `Generated at: ${formatDateTime(generatedAt)} (local time)`;
128-
writeLine(color.dim(generatedAtText), generatedAtText);
105+
writeLine("Token Plan Usage", color.cyan);
106+
writeLine(`Generated at: ${formatDateTime(generatedAt)} (local time)`, color.dim);
129107
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
130108
writeQuota(
131109
"5-hour quota",
132-
"5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。",
133-
usage.per5HourPercentage,
134-
usage.per5HourResetTime,
110+
"The 5-hour limit may be unlimited; verify in the Bailian Token Plan console.",
111+
{ percentage: usage.per5HourPercentage, resetTime: usage.per5HourResetTime },
135112
);
136113
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
137114
writeQuota(
138115
"1-week quota",
139-
"1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。",
140-
usage.per1WeekPercentage,
141-
usage.per1WeekResetTime,
116+
"The 1-week limit may be unlimited; verify in the Bailian Token Plan console.",
117+
{ percentage: usage.per1WeekPercentage, resetTime: usage.per1WeekResetTime },
142118
);
143119
process.stdout.write(`└${"─".repeat(BOX_WIDTH)}┘\n`);
144120
}
145121

146122
export default defineCommand({
147-
description: "Show Token Plan quota usage as core JSON or a human-readable view",
123+
description: "Show Token Plan quota usage",
148124
auth: "console",
149-
usageArgs: "<--json | --view> [flags]",
150-
flags: {
151-
json: {
152-
type: "switch",
153-
description: "Output only the four core usage fields as JSON",
154-
},
155-
view: {
156-
type: "switch",
157-
description: "Render a compact human-readable quota view",
158-
},
159-
},
160-
exampleArgs: ["--json", "--view"],
161-
validate: (flags) =>
162-
flags.json === flags.view ? "Choose exactly one of --json or --view." : undefined,
125+
usageArgs: "[flags]",
126+
exampleArgs: ["", "--output json"],
163127
async run(ctx) {
164-
const { flags, settings } = ctx;
128+
const { settings } = ctx;
129+
const format = detectOutputFormat(settings.output);
165130

166131
if (settings.dryRun) {
167-
emitResult({ api: TOKEN_PLAN_USAGE_API, data: {} }, "json");
132+
emitResult({ api: TOKEN_PLAN_USAGE_API, data: {} }, format);
168133
return;
169134
}
170135

171136
const result = await ctx.client.console(TOKEN_PLAN_USAGE_API, {});
172137
const usage = readUsage(result);
173138

174-
if (flags.json) {
175-
emitResult(usage, "json");
139+
if (format === "json") {
140+
emitResult(usage, format);
176141
return;
177142
}
178143

packages/commands/tests/e2e/usage-token-plan.e2e.test.ts

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,48 +15,37 @@ describe("e2e: usage token-plan", () => {
1515
"--help",
1616
]);
1717
expect(exitCode, stderr).toBe(0);
18-
expect(stderr).toMatch(/--json|--view|Token Plan/i);
18+
expect(stderr).toMatch(/Token Plan|quota/i);
1919
});
2020

21-
test("usage token-plan 未选择输出形式时退出为用法错误", async () => {
21+
test("usage token-plan --help 包含 --output json 示例", async () => {
2222
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
2323
"usage",
2424
"token-plan",
25-
"--quiet",
26-
]);
27-
expect(exitCode).toBe(2);
28-
expect(stderr).toContain("Choose exactly one of --json or --view.");
29-
});
30-
31-
test("usage token-plan 同时选择两种输出形式时退出为用法错误", async () => {
32-
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
33-
"usage",
34-
"token-plan",
35-
"--json",
36-
"--view",
37-
"--quiet",
25+
"--help",
3826
]);
39-
expect(exitCode).toBe(2);
40-
expect(stderr).toContain("Choose exactly one of --json or --view.");
27+
expect(exitCode, stderr).toBe(0);
28+
expect(stderr).toContain("bl usage token-plan --output json");
4129
});
4230
});
4331

4432
describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () => {
45-
test("usage token-plan --json --dry-run 输出网关请求计划", async () => {
33+
test("usage token-plan --dry-run 输出网关请求计划", async () => {
4634
const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
4735
"usage",
4836
"token-plan",
49-
"--json",
5037
"--dry-run",
38+
"--output",
39+
"json",
5140
]);
5241
expect(exitCode, stderr).toBe(0);
5342
const data = parseStdoutJson<{ api?: string; data?: Record<string, unknown> }>(stdout);
5443
expect(data.api).toBe("zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage");
5544
expect(data.data).toEqual({});
5645
});
5746

58-
test("usage token-plan --json 返回可用的额度字段", async () => {
59-
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--json"]);
47+
test("usage token-plan --output json 返回可用的额度字段", async () => {
48+
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--output", "json"]);
6049
if (isConsoleAuthFailure(result)) return;
6150
expect(result.exitCode, result.stderr).toBe(0);
6251
const data = parseStdoutJson<{
@@ -65,22 +54,19 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
6554
per1WeekPercentage?: number;
6655
per1WeekResetTime?: number;
6756
}>(result.stdout);
68-
const quotas = [
69-
[data.per5HourPercentage, data.per5HourResetTime],
70-
[data.per1WeekPercentage, data.per1WeekResetTime],
57+
const fields = [
58+
data.per5HourPercentage,
59+
data.per5HourResetTime,
60+
data.per1WeekPercentage,
61+
data.per1WeekResetTime,
7162
];
72-
for (const [percentage, resetTime] of quotas) {
73-
if (percentage === undefined) expect(resetTime).toBeUndefined();
74-
else if (percentage === 0) expect(resetTime).toBeUndefined();
75-
else {
76-
expect(percentage).toBeTypeOf("number");
77-
expect(resetTime).toBeTypeOf("number");
78-
}
63+
for (const field of fields) {
64+
if (field !== undefined) expect(field).toBeTypeOf("number");
7965
}
8066
});
8167

82-
test("usage token-plan --view 渲染生成时间与两个额度窗口", async () => {
83-
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--view"]);
68+
test("usage token-plan 默认渲染生成时间与两个额度窗口", async () => {
69+
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan"]);
8470
if (isConsoleAuthFailure(result)) return;
8571
expect(result.exitCode, result.stderr).toBe(0);
8672
expect(result.stdout).toContain("Generated at:");

0 commit comments

Comments
 (0)