mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(tokenplan): 添加 tokenplan seats 命令以列出订阅座位详情
新增 tokenplan seats 命令,支持分页和状态过滤,提供详细的座位信息查询功能。相关文档已更新。
This commit is contained in:
@@ -46,6 +46,7 @@ import quotaList from "./quota/list.ts";
|
||||
import quotaRequest from "./quota/request.ts";
|
||||
import quotaHistory from "./quota/history.ts";
|
||||
import quotaCheck from "./quota/check.ts";
|
||||
import tokenplanSeats from "./tokenplan/seats.ts";
|
||||
|
||||
/** Command registry map (no dependency on registry.ts — safe for build-time import). */
|
||||
export const commands: Record<string, Command> = {
|
||||
@@ -94,5 +95,6 @@ export const commands: Record<string, Command> = {
|
||||
"quota request": quotaRequest,
|
||||
"quota history": quotaHistory,
|
||||
"quota check": quotaCheck,
|
||||
"tokenplan seats": tokenplanSeats,
|
||||
update: update,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import {
|
||||
defineCommand,
|
||||
buildCanonicalQuery,
|
||||
signRequest,
|
||||
modelStudioHost,
|
||||
detectOutputFormat,
|
||||
maskToken,
|
||||
trackingHeaders,
|
||||
type Config,
|
||||
type GlobalFlags,
|
||||
type GetSubscriptionSeatDetailsResponse,
|
||||
type TokenPlanSeatDetail,
|
||||
BailianError,
|
||||
ExitCode,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "../../output/output.ts";
|
||||
import { padEnd } from "../../output/cjk-width.ts";
|
||||
|
||||
const API_VERSION = "2026-02-10";
|
||||
const API_ACTION = "GetSubscriptionSeatDetails";
|
||||
const API_PATH = "/tokenplan/subscription/seat-detail";
|
||||
|
||||
export default defineCommand({
|
||||
name: "tokenplan seats",
|
||||
description: "List Token Plan subscription seat details",
|
||||
usage: "bl tokenplan seats [flags]",
|
||||
options: [
|
||||
{ flag: "--page-no <n>", description: "Page number (default: 1)", type: "number" },
|
||||
{ flag: "--page-size <n>", description: "Page size (default: 10)", type: "number" },
|
||||
{
|
||||
flag: "--caller-uac-account-id <id>",
|
||||
description: "Caller UAC account ID",
|
||||
},
|
||||
{
|
||||
flag: "--namespace-id <id>",
|
||||
description: "Product namespace ID (Token Plan default: namespace-1)",
|
||||
},
|
||||
{
|
||||
flag: "--status <status>",
|
||||
description:
|
||||
"Seat status filter (repeatable): CREATING, NORMAL, LIMIT, RELEASE, STOP, REFUNDED",
|
||||
type: "array",
|
||||
},
|
||||
{
|
||||
flag: "--status-list-str <json>",
|
||||
description: "StatusList as JSON string, e.g. '[\"NORMAL\"]'",
|
||||
},
|
||||
{ flag: "--seat-id <id>", description: "Filter by seat ID" },
|
||||
{
|
||||
flag: "--seat-type <type>",
|
||||
description: "Seat tier: standard, pro, or max",
|
||||
},
|
||||
{
|
||||
flag: "--query-assigned <bool>",
|
||||
description: "Filter by assignment: true=assigned, false=unassigned",
|
||||
},
|
||||
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (deprecated)" },
|
||||
{
|
||||
flag: "--access-key-secret <key>",
|
||||
description: "Alibaba Cloud Access Key Secret (deprecated)",
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
"bl tokenplan seats",
|
||||
"bl tokenplan seats --page-size 20 --status NORMAL",
|
||||
"bl tokenplan seats --query-assigned true --seat-type standard",
|
||||
],
|
||||
async run(config: Config, flags: GlobalFlags) {
|
||||
const format = detectOutputFormat(config.output);
|
||||
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
|
||||
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
|
||||
|
||||
if (!accessKeyId || !accessKeySecret) {
|
||||
throw new BailianError(
|
||||
"No credentials found.\n" +
|
||||
"Set ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET.",
|
||||
ExitCode.AUTH,
|
||||
);
|
||||
}
|
||||
|
||||
const queryParams = buildQueryParams(flags);
|
||||
const queryString = buildCanonicalQuery(queryParams);
|
||||
const host = modelStudioHost(config.region);
|
||||
const endpoint = `https://${host}${API_PATH}${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
if (config.dryRun) {
|
||||
emitResult({ endpoint, query: queryParams }, format);
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = signRequest({
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
action: API_ACTION,
|
||||
version: API_VERSION,
|
||||
body: "",
|
||||
host,
|
||||
pathname: API_PATH,
|
||||
method: "GET",
|
||||
queryString,
|
||||
});
|
||||
|
||||
if (config.verbose) {
|
||||
process.stderr.write(`> GET ${endpoint}\n`);
|
||||
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
|
||||
}
|
||||
|
||||
const timeoutMs = config.timeout * 1000;
|
||||
const res = await fetch(endpoint, {
|
||||
method: "GET",
|
||||
headers: { ...headers, ...trackingHeaders() },
|
||||
signal: AbortSignal.timeout(timeoutMs),
|
||||
});
|
||||
|
||||
if (config.verbose) {
|
||||
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as GetSubscriptionSeatDetailsResponse;
|
||||
|
||||
if (!res.ok || data.Success === false) {
|
||||
throw new BailianError(
|
||||
`${data.Code || res.status} - ${data.Message || res.statusText}`,
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
}
|
||||
|
||||
const items = data.Data?.Items ?? [];
|
||||
if (config.quiet || format === "text") {
|
||||
emitTextSeats(items, data.Data?.Total, data.Data?.PageNo, data.Data?.PageSize);
|
||||
} else {
|
||||
emitResult(data, format);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function buildQueryParams(flags: GlobalFlags): Record<string, string | string[] | undefined> {
|
||||
const params: Record<string, string | string[] | undefined> = {};
|
||||
|
||||
if (flags.pageNo !== undefined) params.PageNo = String(flags.pageNo as number);
|
||||
if (flags.pageSize !== undefined) params.PageSize = String(flags.pageSize as number);
|
||||
if (flags.callerUacAccountId) params.CallerUacAccountId = flags.callerUacAccountId as string;
|
||||
if (flags.namespaceId) params.NamespaceId = flags.namespaceId as string;
|
||||
if (flags.statusListStr) params.StatusListStr = flags.statusListStr as string;
|
||||
|
||||
const status = flags.status;
|
||||
if (Array.isArray(status) && status.length > 0) {
|
||||
params.StatusList = status as string[];
|
||||
} else if (typeof status === "string" && status.length > 0) {
|
||||
params.StatusList = [status];
|
||||
}
|
||||
|
||||
if (flags.seatId) params.SeatId = flags.seatId as string;
|
||||
if (flags.seatType) params.SeatType = flags.seatType as string;
|
||||
|
||||
if (typeof flags.queryAssigned === "string" && flags.queryAssigned.length > 0) {
|
||||
params.QueryAssigned = flags.queryAssigned;
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function emitTextSeats(
|
||||
items: TokenPlanSeatDetail[],
|
||||
total?: number,
|
||||
pageNo?: number,
|
||||
pageSize?: number,
|
||||
): void {
|
||||
if (items.length === 0) {
|
||||
emitBare("No seats found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const header = [
|
||||
padEnd("SeatId", 18),
|
||||
padEnd("Type", 10),
|
||||
padEnd("Status", 10),
|
||||
padEnd("Assigned", 12),
|
||||
padEnd("Account", 20),
|
||||
].join(" ");
|
||||
emitBare(header);
|
||||
emitBare("-".repeat(header.length));
|
||||
|
||||
for (const item of items) {
|
||||
const row = [
|
||||
padEnd(item.SeatId ?? "-", 18),
|
||||
padEnd(item.SpecType ?? "-", 10),
|
||||
padEnd(item.Status ?? "-", 10),
|
||||
padEnd(item.AssignedStatus ?? "-", 12),
|
||||
padEnd(item.AccountName ?? item.AccountId ?? "-", 20),
|
||||
].join(" ");
|
||||
emitBare(row);
|
||||
}
|
||||
|
||||
if (total !== undefined) {
|
||||
emitBare("");
|
||||
emitBare(
|
||||
`Total: ${total}${pageNo !== undefined ? ` | Page: ${pageNo}` : ""}${pageSize !== undefined ? ` | PageSize: ${pageSize}` : ""}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ const NO_AUTH_SETUP = [
|
||||
["quota", "request"],
|
||||
["quota", "history"],
|
||||
["quota", "check"],
|
||||
["tokenplan", "seats"],
|
||||
];
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -136,6 +136,15 @@ export function isKnowledgeAkSkReady(): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Token Plan POP commands (AK/SK only). */
|
||||
export function isTokenPlanAkSkReady(): boolean {
|
||||
return (
|
||||
isBailianE2EEnabled() &&
|
||||
!!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID &&
|
||||
!!process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET
|
||||
);
|
||||
}
|
||||
|
||||
export interface RunCliResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { tmpdir } from "os";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isTokenPlanAkSkReady, parseStdoutJson, runCli } from "./helpers.ts";
|
||||
|
||||
interface DryRunBody {
|
||||
endpoint?: string;
|
||||
query?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe("e2e: tokenplan seats", () => {
|
||||
test("tokenplan 分组展示子命令帮助且成功退出", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(["tokenplan"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const out = `${stdout}\n${stderr}`;
|
||||
expect(out).toMatch(/tokenplan|seats/i);
|
||||
});
|
||||
|
||||
test("tokenplan seats --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["tokenplan", "seats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--page-no/i);
|
||||
expect(stderr).toMatch(/--page-size/i);
|
||||
expect(stderr).toMatch(/--seat-id/i);
|
||||
expect(stderr).toMatch(/--status/i);
|
||||
expect(stderr).toMatch(/--query-assigned/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("e2e: tokenplan seats errors", () => {
|
||||
test("无任何凭证时提示 No credentials found 并非零退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(
|
||||
["tokenplan", "seats", "--non-interactive", "--output", "json"],
|
||||
{
|
||||
DASHSCOPE_API_KEY: undefined,
|
||||
DASHSCOPE_ACCESS_TOKEN: undefined,
|
||||
ALIBABA_CLOUD_ACCESS_KEY_ID: undefined,
|
||||
ALIBABA_CLOUD_ACCESS_KEY_SECRET: undefined,
|
||||
BAILIAN_CONFIG_DIR: tmpdir(),
|
||||
},
|
||||
);
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toMatch(/no credentials found/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("e2e: tokenplan seats dry-run", () => {
|
||||
test("--dry-run 输出 endpoint 和 query 参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(
|
||||
[
|
||||
"tokenplan",
|
||||
"seats",
|
||||
"--dry-run",
|
||||
"--page-no",
|
||||
"1",
|
||||
"--page-size",
|
||||
"10",
|
||||
"--status",
|
||||
"NORMAL",
|
||||
"--query-assigned",
|
||||
"true",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
],
|
||||
{
|
||||
ALIBABA_CLOUD_ACCESS_KEY_ID: "LTAI-fake",
|
||||
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "fake-secret",
|
||||
},
|
||||
);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<DryRunBody>(stdout);
|
||||
expect(data.endpoint).toMatch(/\/tokenplan\/subscription\/seat-detail/);
|
||||
expect(data.query?.PageNo).toBe("1");
|
||||
expect(data.query?.PageSize).toBe("10");
|
||||
expect(data.query?.QueryAssigned).toBe("true");
|
||||
expect(data.query?.StatusList).toEqual(["NORMAL"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isTokenPlanAkSkReady())("e2e: tokenplan seats(AK/SK)", () => {
|
||||
test("GetSubscriptionSeatDetails 真实调用", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"tokenplan",
|
||||
"seats",
|
||||
"--page-size",
|
||||
"5",
|
||||
"--non-interactive",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ Success?: boolean; Data?: { Items?: unknown[] } }>(stdout);
|
||||
expect(data.Success).toBe(true);
|
||||
expect(Array.isArray(data.Data?.Items)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,33 @@ export interface AkSignConfig {
|
||||
host: string;
|
||||
pathname: string;
|
||||
method?: string;
|
||||
/** ACS3 canonical query string (sorted, encoded, no leading `?`). Empty for POST body-only APIs. */
|
||||
queryString?: string;
|
||||
}
|
||||
|
||||
/** Build ACS3 canonical query string from POP query parameters. */
|
||||
export function buildCanonicalQuery(params: Record<string, string | string[] | undefined>): string {
|
||||
const pairs: Array<[string, string]> = [];
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === "") continue;
|
||||
if (Array.isArray(value)) {
|
||||
const sorted = [...value].sort();
|
||||
for (const v of sorted) {
|
||||
if (v !== "") pairs.push([key, v]);
|
||||
}
|
||||
} else {
|
||||
pairs.push([key, value]);
|
||||
}
|
||||
}
|
||||
pairs.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
return pairs.map(([k, v]) => `${encodeRFC3986(k)}=${encodeRFC3986(v)}`).join("&");
|
||||
}
|
||||
|
||||
function encodeRFC3986(str: string): string {
|
||||
return encodeURIComponent(str).replace(
|
||||
/[!'()*]/g,
|
||||
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function signRequest(cfg: AkSignConfig): Record<string, string> {
|
||||
@@ -47,11 +74,13 @@ export function signRequest(cfg: AkSignConfig): Record<string, string> {
|
||||
|
||||
const signedHeadersStr = signedHeaderKeys.join(";");
|
||||
|
||||
const queryString = cfg.queryString ?? "";
|
||||
|
||||
// Build canonical request
|
||||
const canonicalRequest = [
|
||||
method,
|
||||
cfg.pathname,
|
||||
"", // query string (empty for POST)
|
||||
queryString,
|
||||
canonicalHeaders,
|
||||
signedHeadersStr,
|
||||
hashedBody,
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
import type { Region } from "../config/schema.ts";
|
||||
|
||||
const MODEL_STUDIO_HOSTS: Record<Region, string> = {
|
||||
cn: "modelstudio.cn-beijing.aliyuncs.com",
|
||||
us: "modelstudio.cn-beijing.aliyuncs.com",
|
||||
intl: "modelstudio.ap-southeast-1.aliyuncs.com",
|
||||
};
|
||||
|
||||
/** ModelStudio POP OpenAPI host for the given DashScope region preset. */
|
||||
export function modelStudioHost(region: Region): string {
|
||||
return MODEL_STUDIO_HOSTS[region] ?? MODEL_STUDIO_HOSTS.cn;
|
||||
}
|
||||
|
||||
// ---- Chat (OpenAI Compatible) ----
|
||||
|
||||
export function chatEndpoint(baseUrl: string): string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type { AkSignConfig } from "./ak-sign.ts";
|
||||
export { signRequest } from "./ak-sign.ts";
|
||||
export { buildCanonicalQuery, signRequest } from "./ak-sign.ts";
|
||||
export {
|
||||
appCompletionEndpoint,
|
||||
chatEndpoint,
|
||||
@@ -10,6 +10,7 @@ export {
|
||||
memoryListEndpoint,
|
||||
memoryNodeEndpoint,
|
||||
memorySearchEndpoint,
|
||||
modelStudioHost,
|
||||
mcpWebSearchEndpoint,
|
||||
profileSchemaEndpoint,
|
||||
speechRecognizeEndpoint,
|
||||
|
||||
@@ -417,6 +417,44 @@ export interface DashScopeKnowledgeRetrieveResponse {
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Token Plan / ModelStudio POP (2026-02-10) ----
|
||||
|
||||
export interface TokenPlanSeatEquity {
|
||||
EquityType?: string;
|
||||
CycleInstanceId?: string;
|
||||
CycleStartTime?: number;
|
||||
CycleEndTime?: number;
|
||||
CycleTotalValue?: number;
|
||||
CycleSurplusValue?: number;
|
||||
CycleVersion?: number;
|
||||
}
|
||||
|
||||
export interface TokenPlanSeatDetail {
|
||||
InstanceCode?: string;
|
||||
EquityList?: TokenPlanSeatEquity[];
|
||||
EndTime?: number;
|
||||
SeatId?: string;
|
||||
SpecType?: string;
|
||||
StartTime?: number;
|
||||
AssignedStatus?: string;
|
||||
AccountId?: string;
|
||||
AccountName?: string;
|
||||
AccountEmail?: string;
|
||||
Status?: string;
|
||||
}
|
||||
|
||||
export interface GetSubscriptionSeatDetailsResponse {
|
||||
Success?: boolean;
|
||||
Code?: string;
|
||||
Message?: string;
|
||||
Data?: {
|
||||
Items?: TokenPlanSeatDetail[];
|
||||
Total?: number;
|
||||
PageNo?: number;
|
||||
PageSize?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Speech Synthesis / TTS (DashScope) ----
|
||||
|
||||
export interface DashScopeTTSRequest {
|
||||
|
||||
@@ -45,6 +45,7 @@ Use this index for the full quick index and global flags.
|
||||
| `bl speech recognize` | Recognize speech from audio files (FunAudio-ASR) | [speech.md](speech.md) |
|
||||
| `bl speech synthesize` | Synthesize speech from text (CosyVoice TTS) | [speech.md](speech.md) |
|
||||
| `bl text chat` | Send a chat completion (OpenAI compatible, DashScope) | [text.md](text.md) |
|
||||
| `bl tokenplan seats` | List Token Plan subscription seat details | [tokenplan.md](tokenplan.md) |
|
||||
| `bl update` | Update bl to the latest version | [update.md](update.md) |
|
||||
| `bl usage free` | Query free-tier quota for models (all models if --model is omitted) | [usage.md](usage.md) |
|
||||
| `bl usage freetier` | Enable or disable auto-stop for free-tier models. Enables by default; use --off to disable | [usage.md](usage.md) |
|
||||
@@ -77,6 +78,7 @@ Use this index for the full quick index and global flags.
|
||||
| `search` | `web` | [search.md](search.md) |
|
||||
| `speech` | `recognize`, `synthesize` | [speech.md](speech.md) |
|
||||
| `text` | `chat` | [text.md](text.md) |
|
||||
| `tokenplan` | `seats` | [tokenplan.md](tokenplan.md) |
|
||||
| `update` | `(root)` | [update.md](update.md) |
|
||||
| `usage` | `free`, `freetier`, `stats` | [usage.md](usage.md) |
|
||||
| `video` | `download`, `edit`, `generate`, `ref`, `task get` | [video.md](video.md) |
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# `bl tokenplan` commands
|
||||
|
||||
> Auto-generated from `packages/cli/src/commands/catalog.ts`. Do not edit by hand.
|
||||
> Regenerate: `pnpm --filter bailian-cli run generate:reference`.
|
||||
|
||||
Index: [index.md](index.md)
|
||||
|
||||
## Commands in this group
|
||||
|
||||
| Command | Description |
|
||||
| -------------------- | ----------------------------------------- |
|
||||
| `bl tokenplan seats` | List Token Plan subscription seat details |
|
||||
|
||||
## Command details
|
||||
|
||||
### `bl tokenplan seats`
|
||||
|
||||
| Field | Value |
|
||||
| --------------- | ----------------------------------------- |
|
||||
| **Name** | `tokenplan seats` |
|
||||
| **Description** | List Token Plan subscription seat details |
|
||||
| **Usage** | `bl tokenplan seats [flags]` |
|
||||
|
||||
#### Options
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | --------------------------------------------------------------------------------- |
|
||||
| `--page-no <n>` | number | no | Page number (default: 1) |
|
||||
| `--page-size <n>` | number | no | Page size (default: 10) |
|
||||
| `--caller-uac-account-id <id>` | string | no | Caller UAC account ID |
|
||||
| `--namespace-id <id>` | string | no | Product namespace ID (Token Plan default: namespace-1) |
|
||||
| `--status <status>` | array | no | Seat status filter (repeatable): CREATING, NORMAL, LIMIT, RELEASE, STOP, REFUNDED |
|
||||
| `--status-list-str <json>` | string | no | StatusList as JSON string, e.g. '["NORMAL"]' |
|
||||
| `--seat-id <id>` | string | no | Filter by seat ID |
|
||||
| `--seat-type <type>` | string | no | Seat tier: standard, pro, or max |
|
||||
| `--query-assigned <bool>` | string | no | Filter by assignment: true=assigned, false=unassigned |
|
||||
| `--access-key-id <key>` | string | no | Alibaba Cloud Access Key ID (deprecated) |
|
||||
| `--access-key-secret <key>` | string | no | Alibaba Cloud Access Key Secret (deprecated) |
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
bl tokenplan seats
|
||||
```
|
||||
|
||||
```bash
|
||||
bl tokenplan seats --page-size 20 --status NORMAL
|
||||
```
|
||||
|
||||
```bash
|
||||
bl tokenplan seats --query-assigned true --seat-type standard
|
||||
```
|
||||
Reference in New Issue
Block a user