Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and

[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)

## [1.25.0] - 2026-09-14

### Added

- **Token Plan harness quota** — `bl token-plan harness-quota` shows Token Plan harness entitlement quota usage (Console auth), joining the harness list with issued entitlements to display used/total quota, usage ratio, and reset time; harnesses with a pending entitlement are listed as issuing.
- Filter the harness list with `--type official_tool|infrastructure`; render as a quota box or `--output json`.

## [1.24.0] - 2026-09-11

### Added
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@

[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)

## [1.25.0] - 2026-09-14

### 新增

- **Token Plan harness 权益额度** —— `bl token-plan harness-quota` 查看 Token Plan harness 权益额度用量(Console 认证),联合 harness 列表与已发放权益,展示已用/总额度、使用比例和重置时间;待发放权益的 harness 以「发放中」状态列出。
- 通过 `--type official_tool|infrastructure` 筛选 harness 列表;支持额度框输出或 `--output json`。

## [1.24.0] - 2026-09-11

### 新增
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bailian-cli",
"version": "1.24.0",
"version": "1.25.0",
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
"keywords": [
"agent",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ import {
tokenPlanCreateKey,
tokenPlanAssignSeats,
tokenPlanAddMember,
tokenPlanHarnessQuota,
workspaceInit,
pluginInstall,
pluginLink,
Expand Down Expand Up @@ -366,6 +367,7 @@ export const commands: Record<string, AnyCommand> = {
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
"token-plan add-member": tokenPlanAddMember,
"token-plan harness-quota": tokenPlanHarnessQuota,
"workspace init": workspaceInit,
"plugin install": pluginInstall,
"plugin link": pluginLink,
Expand Down
2 changes: 1 addition & 1 deletion packages/commands/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bailian-cli-commands",
"version": "1.24.0",
"version": "1.25.0",
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
"homepage": "https://bailian.console.aliyun.com/cli",
"bugs": {
Expand Down
169 changes: 169 additions & 0 deletions packages/commands/src/commands/token-plan/harness-quota.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { randomUUID } from "node:crypto";
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { printQuotaBox, readNumber, type QuotaSection } from "../usage/quota-box.ts";
import { formatNumber } from "../shared/format.ts";
import type { HarnessBenefitItem, TokenPlanEquityInfo } from "./types.ts";

const HARNESS_LIST_API = "zeldaEasy.broadscope-bailian.token-plan.detail";
const EQUITY_INFO_API = "zeldaEasy.bailian-commerce.tokenPlan.queryTokenPlanEquityInfo";

/** One harness that carries an entitlement quota, issued or still pending. */
interface HarnessQuotaRow {
planCode: string;
title: string;
type: string;
/** `issuing` means the harness carries a quota but no entitlement is issued yet. */
status: "issued" | "issuing";
quotaUnit: string;
instanceId?: string;
totalQuota?: number;
availableQuota?: number;
usedQuota?: number;
/** Used ratio in percent with 0.1 precision, matching the console display. */
usedPercent?: number;
instanceStartTime?: number;
instanceEndTime?: number;
}

function readArray<T>(result: unknown, field: string): T[] {
const response = unwrapResponse(result as Record<string, unknown>);
const value = response[field];
return Array.isArray(value) ? (value as T[]) : [];
}

/**
* Join the harness list with the issued entitlements, keeping the server order.
* A harness carrying a resource pack but no matching entitlement is still
* listed as `issuing` — issuance lags the purchase by a few minutes.
*/
function buildRows(
items: HarnessBenefitItem[],
equityInfos: TokenPlanEquityInfo[],
): HarnessQuotaRow[] {
const rows: HarnessQuotaRow[] = [];

for (const item of items) {
// Only harnesses that carry a resource pack have an entitlement quota at all.
if (item.hasResourcePack !== true) continue;

const planCodes = item.planCodes ?? [];
const equity = equityInfos.find(
(equityInfo) => equityInfo.equityType && planCodes.includes(equityInfo.equityType),
);
const planCode = equity?.equityType ?? planCodes[0] ?? "";
const row: HarnessQuotaRow = {
planCode,
title: item.title ?? planCode,
type: item.type ?? "",
status: equity ? "issued" : "issuing",
quotaUnit: item.quotaUnit ?? item.priceInfo?.unit ?? "",
};

if (!equity) {
rows.push(row);
continue;
}

if (equity.instanceId) row.instanceId = equity.instanceId;
const totalQuota = readNumber(equity.totalQuota);
if (totalQuota !== undefined) row.totalQuota = totalQuota;
const availableQuota = readNumber(equity.availableQuota);
if (availableQuota !== undefined) row.availableQuota = availableQuota;
if (totalQuota !== undefined && availableQuota !== undefined) {
row.usedQuota = Math.max(totalQuota - availableQuota, 0);
if (totalQuota > 0) {
row.usedPercent = Math.round((row.usedQuota / totalQuota) * 1000) / 10;
}
}
const instanceStartTime = readNumber(equity.instanceStartTime);
if (instanceStartTime !== undefined) row.instanceStartTime = instanceStartTime;
const instanceEndTime = readNumber(equity.instanceEndTime);
if (instanceEndTime !== undefined) row.instanceEndTime = instanceEndTime;

rows.push(row);
}

return rows;
}

function toSection(row: HarnessQuotaRow): QuotaSection {
const section: QuotaSection = {
label: `${row.title} (${row.planCode})`,
emptyMessage:
row.status === "issuing"
? "Quota is being issued; issuance can take up to 5 minutes."
: "No positive quota total reported; check the Bailian Token Plan console.",
};
if (row.status === "issuing") return section;

if (row.totalQuota !== undefined && row.usedQuota !== undefined) {
const unitSuffix = row.quotaUnit ? ` ${row.quotaUnit}` : "";
section.detail = `Used: ${formatNumber(row.usedQuota)} / ${formatNumber(row.totalQuota)}${unitSuffix}`;
if (row.totalQuota > 0) section.percentage = row.usedQuota / row.totalQuota;
}
if (row.instanceEndTime !== undefined) section.resetTime = row.instanceEndTime;

return section;
}

export default defineCommand({
description: {
"en-US": "Show Token Plan harness entitlement quota usage",
"zh-CN": "查看 Token Plan harness 权益额度用量",
},
auth: "console",
usageArgs: "[flags]",
flags: {
type: {
type: "string",
valueHint: "<type>",
choices: ["official_tool", "infrastructure"] as const,
description: {
"en-US": "Filter harness list by type: official_tool, infrastructure",
"zh-CN": "按类型筛选 harness 列表:official_tool、infrastructure",
},
},
},
exampleArgs: ["", "--type official_tool", "--output json"],
async run(ctx) {
const { settings, flags } = ctx;
const format = detectOutputFormat(settings.output);
const benefitsData = flags.type ? { type: flags.type } : {};
const equityData = { queryTokenPlanEquityInfoRequest: { requestId: randomUUID() } };

if (settings.dryRun) {
emitResult(
{
requests: [
{ api: HARNESS_LIST_API, data: benefitsData },
{ api: EQUITY_INFO_API, data: equityData },
],
},
format,
);
return;
}

const [benefitsResult, equityResult] = await Promise.all([
ctx.client.console(HARNESS_LIST_API, benefitsData),
ctx.client.console(EQUITY_INFO_API, equityData),
]);
const rows = buildRows(
readArray<HarnessBenefitItem>(benefitsResult, "items"),
readArray<TokenPlanEquityInfo>(equityResult, "tokenPlanEquityInfos"),
);

if (format === "json") {
emitResult({ generatedAt: Date.now(), items: rows }, format);
return;
}

if (rows.length === 0) {
process.stdout.write("No harness with entitlement quota found.\n");
return;
}

printQuotaBox("Token Plan Harness Quota", rows.map(toSection), Date.now());
},
});
22 changes: 22 additions & 0 deletions packages/commands/src/commands/token-plan/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,25 @@ export interface AddOrganizationMemberResponse {
SeatAssigned?: boolean;
};
}

// Console gateway payloads keep the server's camelCase keys, unlike the
// PascalCase OpenAPI shapes above.

export interface HarnessBenefitItem {
type?: string;
hasResourcePack?: boolean;
planCodes?: string[];
title?: string;
quotaUnit?: string;
priceInfo?: { unit?: string } | null;
}

export interface TokenPlanEquityInfo {
instanceId?: string;
templateCode?: string;
equityType?: string;
totalQuota?: number;
availableQuota?: number;
instanceStartTime?: number;
instanceEndTime?: number;
}
1 change: 1 addition & 0 deletions packages/commands/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export { default as tokenPlanListSeats } from "./commands/token-plan/list-seats.
export { default as tokenPlanCreateKey } from "./commands/token-plan/create-key.ts";
export { default as tokenPlanAssignSeats } from "./commands/token-plan/assign-seats.ts";
export { default as tokenPlanAddMember } from "./commands/token-plan/add-member.ts";
export { default as tokenPlanHarnessQuota } from "./commands/token-plan/harness-quota.ts";
export { default as managedAgentInit } from "./commands/managed-agent/init.ts";
export { default as managedAgentValidate } from "./commands/managed-agent/validate.ts";
export { default as managedAgentPlan } from "./commands/managed-agent/plan.ts";
Expand Down
97 changes: 96 additions & 1 deletion packages/commands/tests/e2e/token-plan.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { describe, expect, test } from "vite-plus/test";
import { makeE2eOutputDir, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
import {
isConsoleAuthFailure,
isConsoleE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { TOKEN_PLAN_ROUTES } from "./topic-routes.ts";

describe("e2e: token-plan", () => {
Expand Down Expand Up @@ -60,4 +67,92 @@ describe("e2e: token-plan", () => {
expect(stderr).toMatch(/ALIBABA_CLOUD_ACCESS_KEY_ID/);
expect(stderr).not.toMatch(/auth login --api-key/);
});

test("token-plan harness-quota help 展示 --type 与 console 鉴权域 flags", async () => {
const { stderr, exitCode } = await runCommandHelp(TOKEN_PLAN_ROUTES, [
"token-plan",
"harness-quota",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--type <official_tool\|infrastructure>/);
expect(stderr).toMatch(/--console-region/);
expect(stderr).not.toMatch(/--access-key-id/);
});
});

describe.skipIf(!isConsoleE2EReady())("e2e: token-plan harness-quota(Console)", () => {
test("harness-quota --dry-run 输出两个网关请求计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(TOKEN_PLAN_ROUTES, [
"token-plan",
"harness-quota",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
requests?: Array<{ api?: string; data?: Record<string, unknown> }>;
}>(stdout);
expect(data.requests?.[0]?.api).toBe("zeldaEasy.broadscope-bailian.token-plan.detail");
expect(data.requests?.[1]?.api).toBe(
"zeldaEasy.bailian-commerce.tokenPlan.queryTokenPlanEquityInfo",
);
expect(data.requests?.[0]?.data).toEqual({});
});

test("harness-quota --type 透传给 harness 列表接口", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(TOKEN_PLAN_ROUTES, [
"token-plan",
"harness-quota",
"--type",
"official_tool",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ requests?: Array<{ data?: Record<string, unknown> }> }>(stdout);
expect(data.requests?.[0]?.data).toEqual({ type: "official_tool" });
});

test("harness-quota --output json 返回权益额度条目", async () => {
const result = await runCommandE2e(TOKEN_PLAN_ROUTES, [
"token-plan",
"harness-quota",
"--output",
"json",
]);
if (isConsoleAuthFailure(result)) return;
expect(result.exitCode, result.stderr).toBe(0);
const data = parseStdoutJson<{
generatedAt?: number;
items?: Array<{
planCode?: string;
status?: string;
totalQuota?: number;
availableQuota?: number;
usedQuota?: number;
usedPercent?: number;
}>;
}>(result.stdout);
expect(Array.isArray(data.items)).toBe(true);
for (const item of data.items ?? []) {
expect(item.planCode).toBeTypeOf("string");
expect(["issued", "issuing"]).toContain(item.status);
const numbers = [item.totalQuota, item.availableQuota, item.usedQuota, item.usedPercent];
for (const value of numbers) {
if (value !== undefined) expect(value).toBeTypeOf("number");
}
}
});

test("harness-quota 默认渲染额度框或空态文案", async () => {
const result = await runCommandE2e(TOKEN_PLAN_ROUTES, ["token-plan", "harness-quota"]);
if (isConsoleAuthFailure(result)) return;
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout).toMatch(
/Token Plan Harness Quota|No harness with entitlement quota found/,
);
});
});
1 change: 1 addition & 0 deletions packages/commands/tests/e2e/topic-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export const TOKEN_PLAN_ROUTES: E2eRouteExports = {
"token-plan create-key": "tokenPlanCreateKey",
"token-plan assign-seats": "tokenPlanAssignSeats",
"token-plan add-member": "tokenPlanAddMember",
"token-plan harness-quota": "tokenPlanHarnessQuota",
};

export const SKILL_ROUTES: E2eRouteExports = {
Expand Down
Loading