Merge pull request #74 from budiga/token-plan-openapi

Token plan openapi
This commit is contained in:
ls
2026-06-24 12:33:51 +08:00
committed by GitHub
11 changed files with 1049 additions and 50 deletions
+8 -2
View File
@@ -122,6 +122,12 @@ bl quota check # 查看当前用量 vs
bl quota check --model qwen3.6-plus --period 5 # 查看最近 5 分钟用量
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
bl quota history # 查看提额历史记录
# Token Plan 团队版管理(需 AK/SK,见下方认证说明)
bl tokenplan seats # 查看订阅席位明细
bl tokenplan add-member --account-name dev --org-id org_xxx
bl tokenplan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
bl tokenplan create-key --account-id acc_xxx --workspace-id ws_xxx
```
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
@@ -151,9 +157,9 @@ bl text chat --api-key sk-xxxxx --message "你好"
bl auth login --console
```
### 阿里云 AK/SK(仅知识库检索)
### 阿里云 AK/SK(知识库检索与 Token Plan)
`knowledge retrieve` 命令需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
`knowledge retrieve` 与 `tokenplan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
+8
View File
@@ -46,6 +46,10 @@ 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";
import tokenplanCreateKey from "./tokenplan/create-key.ts";
import tokenplanAssignSeats from "./tokenplan/assign-seats.ts";
import tokenplanAddMember from "./tokenplan/add-member.ts";
/** Command registry map (no dependency on registry.ts — safe for build-time import). */
export const commands: Record<string, Command> = {
@@ -94,5 +98,9 @@ export const commands: Record<string, Command> = {
"quota request": quotaRequest,
"quota history": quotaHistory,
"quota check": quotaCheck,
"tokenplan seats": tokenplanSeats,
"tokenplan create-key": tokenplanCreateKey,
"tokenplan assign-seats": tokenplanAssignSeats,
"tokenplan add-member": tokenplanAddMember,
update: update,
};
@@ -0,0 +1,116 @@
import {
defineCommand,
detectOutputFormat,
type Config,
type GlobalFlags,
BailianError,
ExitCode,
} from "bailian-cli-core";
import { emitResult, emitBare } from "../../output/output.ts";
import { padEnd } from "../../output/cjk-width.ts";
import type { AddOrganizationMemberResponse } from "./types.ts";
import {
TOKEN_PLAN_AK_OPTIONS,
TOKEN_PLAN_COMMON_QUERY_OPTIONS,
appendCommonQueryParams,
callTokenPlanApi,
prepareTokenPlanRequest,
resolveTokenPlanCredentials,
type TokenPlanQueryParams,
} from "./utils.ts";
const API_ACTION = "AddOrganizationMember";
const API_PATH = "/tokenplan/organization/member-additions";
const DEFAULT_ORG_ROLE = "ORG_MEMBER";
export default defineCommand({
name: "tokenplan add-member",
description: "Add a member to a Token Plan organization",
usage: "bl tokenplan add-member --account-name <name> --org-id <id> [flags]",
options: [
{ flag: "--account-name <name>", description: "Member display name", required: true },
{ flag: "--org-id <id>", description: "Organization ID", required: true },
{
flag: "--org-role-code <code>",
description: "Organization role: ORG_ADMIN or ORG_MEMBER (default: ORG_MEMBER)",
},
{
flag: "--spec-type <type>",
description: "Seat tier to assign on creation: standard, pro, or max",
},
...TOKEN_PLAN_COMMON_QUERY_OPTIONS,
...TOKEN_PLAN_AK_OPTIONS,
],
examples: [
"bl tokenplan add-member --account-name dev_user --org-id org_123",
"bl tokenplan add-member --account-name admin_user --org-id org_123 --org-role-code ORG_ADMIN",
"bl tokenplan add-member --account-name member1 --org-id org_123 --spec-type standard",
],
async run(config: Config, flags: GlobalFlags) {
const format = detectOutputFormat(config.output);
const credentials = resolveTokenPlanCredentials(config, flags);
const accountName = flags.accountName as string | undefined;
const orgId = flags.orgId as string | undefined;
if (!accountName) {
throw new BailianError("Missing required argument --account-name.", ExitCode.USAGE);
}
if (!orgId) {
throw new BailianError("Missing required argument --org-id.", ExitCode.USAGE);
}
const queryParams = buildQueryParams(flags);
if (config.dryRun) {
const { endpoint, queryParams: query } = prepareTokenPlanRequest(
config,
API_PATH,
queryParams,
);
emitResult({ endpoint, query }, format);
return;
}
const data = await callTokenPlanApi<AddOrganizationMemberResponse>({
config,
credentials,
action: API_ACTION,
path: API_PATH,
method: "POST",
queryParams,
});
if (config.quiet || format === "text") {
emitTextMember(data);
} else {
emitResult(data, format);
}
},
});
function buildQueryParams(flags: GlobalFlags): TokenPlanQueryParams {
const params: TokenPlanQueryParams = {};
if (flags.accountName) params.AccountName = flags.accountName as string;
if (flags.orgId) params.OrgId = flags.orgId as string;
params.OrgRoleCode =
typeof flags.orgRoleCode === "string" && flags.orgRoleCode.length > 0
? flags.orgRoleCode
: DEFAULT_ORG_ROLE;
if (flags.specType) params.SpecType = flags.specType as string;
appendCommonQueryParams(params, flags);
return params;
}
function emitTextMember(data: AddOrganizationMemberResponse): void {
const item = data.Data;
if (!item) {
emitBare("Member added.");
return;
}
emitBare(`${padEnd("AccountId", 14)} ${item.AccountId ?? "-"}`);
emitBare(`${padEnd("SeatAssigned", 14)} ${String(item.SeatAssigned ?? "-")}`);
}
@@ -0,0 +1,103 @@
/**
* ACS3-HMAC-SHA256 signing for ModelStudio Token Plan POP APIs (query-string style).
*
* Extends the core ROA signer with canonical query string support required by
* Token Plan endpoints that pass parameters in the URL query.
*/
import { createHmac, createHash, randomUUID } from "crypto";
export interface TokenPlanAkSignConfig {
accessKeyId: string;
accessKeySecret: string;
action: string;
version: string;
body: string;
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)) {
for (let i = 0; i < value.length; i++) {
const v = value[i];
if (v !== "") pairs.push([`${key}.${i + 1}`, 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 signTokenPlanRequest(cfg: TokenPlanAkSignConfig): Record<string, string> {
const method = cfg.method ?? "POST";
const now = new Date();
const dateISO = now.toISOString().replace(/\.\d{3}Z$/, "Z");
const nonce = randomUUID();
const hashedBody = sha256Hex(cfg.body);
const headers: Record<string, string> = {
host: cfg.host,
"x-acs-action": cfg.action,
"x-acs-version": cfg.version,
"x-acs-date": dateISO,
"x-acs-signature-nonce": nonce,
"x-acs-content-sha256": hashedBody,
"content-type": "application/json",
};
const signedHeaderKeys = Object.keys(headers)
.filter((k) => k === "host" || k === "content-type" || k.startsWith("x-acs-"))
.sort();
const canonicalHeaders = signedHeaderKeys.map((k) => `${k}:${headers[k]}`).join("\n") + "\n";
const signedHeadersStr = signedHeaderKeys.join(";");
const queryString = cfg.queryString ?? "";
const canonicalRequest = [
method,
cfg.pathname,
queryString,
canonicalHeaders,
signedHeadersStr,
hashedBody,
].join("\n");
const algorithm = "ACS3-HMAC-SHA256";
const hashedCanonical = sha256Hex(canonicalRequest);
const stringToSign = `${algorithm}\n${hashedCanonical}`;
const signature = hmacSHA256Hex(cfg.accessKeySecret, stringToSign);
headers["authorization"] =
`${algorithm} Credential=${cfg.accessKeyId},SignedHeaders=${signedHeadersStr},Signature=${signature}`;
return headers;
}
function sha256Hex(data: string): string {
return createHash("sha256").update(data, "utf8").digest("hex");
}
function hmacSHA256Hex(key: string, data: string): string {
return createHmac("sha256", key).update(data, "utf8").digest("hex");
}
@@ -0,0 +1,112 @@
import {
defineCommand,
detectOutputFormat,
type Config,
type GlobalFlags,
BailianError,
ExitCode,
} from "bailian-cli-core";
import { emitResult, emitBare } from "../../output/output.ts";
import type { BatchAssignSeatsResponse } from "./types.ts";
import {
TOKEN_PLAN_AK_OPTIONS,
TOKEN_PLAN_COMMON_QUERY_OPTIONS,
TOKEN_PLAN_WORKSPACE_OPTION,
appendCommonQueryParams,
callTokenPlanApi,
prepareTokenPlanRequest,
requireWorkspaceId,
resolveTokenPlanCredentials,
type TokenPlanQueryParams,
} from "./utils.ts";
const API_ACTION = "BatchAssignSeats";
const API_PATH = "/tokenplan/subscription/seat-assignments";
export default defineCommand({
name: "tokenplan assign-seats",
description: "Batch assign Token Plan seats to members",
usage:
"bl tokenplan assign-seats --workspace-id <id> --seat-type <type> --account-id <id> [flags]",
options: [
TOKEN_PLAN_WORKSPACE_OPTION,
{
flag: "--seat-type <type>",
description: "Seat tier: standard, pro, or max",
required: true,
},
{
flag: "--account-id <id>",
description: "Target member account ID (repeatable)",
type: "array",
},
...TOKEN_PLAN_COMMON_QUERY_OPTIONS,
{
flag: "--locale <locale>",
description: "Language: zh-CN or en-US",
},
...TOKEN_PLAN_AK_OPTIONS,
],
examples: [
"bl tokenplan assign-seats --workspace-id ws_456 --seat-type standard --account-id acc_123",
"bl tokenplan assign-seats --workspace-id ws_456 --seat-type pro --account-id acc_1 --account-id acc_2",
],
async run(config: Config, flags: GlobalFlags) {
const format = detectOutputFormat(config.output);
const credentials = resolveTokenPlanCredentials(config, flags);
const workspaceId = requireWorkspaceId(config, flags);
const seatType = flags.seatType as string | undefined;
if (!seatType) {
throw new BailianError("Missing required argument --seat-type.", ExitCode.USAGE);
}
const accountIds = flags.accountId as string[] | undefined;
if (!accountIds || accountIds.length === 0) {
throw new BailianError("Missing required argument --account-id.", ExitCode.USAGE);
}
const queryParams = buildQueryParams(flags, workspaceId);
if (config.dryRun) {
const { endpoint, queryParams: query } = prepareTokenPlanRequest(
config,
API_PATH,
queryParams,
);
emitResult({ endpoint, query }, format);
return;
}
const data = await callTokenPlanApi<BatchAssignSeatsResponse>({
config,
credentials,
action: API_ACTION,
path: API_PATH,
method: "POST",
queryParams,
});
if (config.quiet || format === "text") {
emitBare("Seats assigned successfully.");
} else {
emitResult(data, format);
}
},
});
function buildQueryParams(flags: GlobalFlags, workspaceId: string): TokenPlanQueryParams {
const params: TokenPlanQueryParams = {};
params.WorkspaceId = workspaceId;
if (flags.seatType) params.SeatType = flags.seatType as string;
appendCommonQueryParams(params, flags);
if (flags.locale) params.Locale = flags.locale as string;
const accountIds = flags.accountId as string[] | undefined;
if (accountIds && accountIds.length > 0) {
params.AccountIds = accountIds;
}
return params;
}
@@ -0,0 +1,111 @@
import {
defineCommand,
detectOutputFormat,
type Config,
type GlobalFlags,
BailianError,
ExitCode,
} from "bailian-cli-core";
import { emitResult, emitBare } from "../../output/output.ts";
import { padEnd } from "../../output/cjk-width.ts";
import type { CreateTokenPlanKeyResponse } from "./types.ts";
import {
TOKEN_PLAN_AK_OPTIONS,
TOKEN_PLAN_COMMON_QUERY_OPTIONS,
TOKEN_PLAN_WORKSPACE_OPTION,
appendCommonQueryParams,
callTokenPlanApi,
prepareTokenPlanRequest,
requireWorkspaceId,
resolveTokenPlanCredentials,
type TokenPlanQueryParams,
} from "./utils.ts";
const API_ACTION = "CreateTokenPlanKey";
const API_PATH = "/tokenplan/api-keys";
export default defineCommand({
name: "tokenplan create-key",
description: "Create a Token Plan API key for a seat",
usage: "bl tokenplan create-key --account-id <id> --workspace-id <id> [flags]",
options: [
{ flag: "--account-id <id>", description: "Target member account ID", required: true },
TOKEN_PLAN_WORKSPACE_OPTION,
{ flag: "--description <text>", description: "API key description" },
...TOKEN_PLAN_COMMON_QUERY_OPTIONS,
...TOKEN_PLAN_AK_OPTIONS,
],
examples: [
"bl tokenplan create-key --account-id acc_123 --workspace-id ws_456",
"bl tokenplan create-key --account-id acc_123 --workspace-id ws_456 --description 'Dev key'",
],
async run(config: Config, flags: GlobalFlags) {
const format = detectOutputFormat(config.output);
const credentials = resolveTokenPlanCredentials(config, flags);
const accountId = flags.accountId as string | undefined;
const workspaceId = requireWorkspaceId(config, flags);
if (!accountId) {
throw new BailianError("Missing required argument --account-id.", ExitCode.USAGE);
}
const queryParams = buildQueryParams(flags, { accountId, workspaceId });
if (config.dryRun) {
const { endpoint, queryParams: query } = prepareTokenPlanRequest(
config,
API_PATH,
queryParams,
);
emitResult({ endpoint, query }, format);
return;
}
const data = await callTokenPlanApi<CreateTokenPlanKeyResponse>({
config,
credentials,
action: API_ACTION,
path: API_PATH,
method: "POST",
queryParams,
});
if (config.quiet || format === "text") {
emitTextKey(data);
} else {
emitResult(data, format);
}
},
});
function buildQueryParams(
flags: GlobalFlags,
resolved: { accountId: string; workspaceId: string },
): TokenPlanQueryParams {
const params: TokenPlanQueryParams = {};
params.AccountId = resolved.accountId;
params.WorkspaceId = resolved.workspaceId;
if (flags.description) params.Description = flags.description as string;
appendCommonQueryParams(params, flags);
return params;
}
function emitTextKey(data: CreateTokenPlanKeyResponse): void {
const item = data.Data;
if (!item) {
emitBare("API key created.");
return;
}
emitBare(`${padEnd("ApiKeyId", 14)} ${item.ApiKeyId ?? "-"}`);
emitBare(`${padEnd("MaskedApiKey", 14)} ${item.MaskedApiKey ?? "-"}`);
if (item.Description) {
emitBare(`${padEnd("Description", 14)} ${item.Description}`);
}
if (item.PlainApiKey) {
emitBare("");
emitBare(`PlainApiKey (shown once): ${item.PlainApiKey}`);
}
}
@@ -0,0 +1,157 @@
import {
defineCommand,
detectOutputFormat,
type Config,
type GlobalFlags,
BailianError,
ExitCode,
} from "bailian-cli-core";
import { emitResult, emitBare } from "../../output/output.ts";
import { padEnd } from "../../output/cjk-width.ts";
import type { GetSubscriptionSeatDetailsResponse, TokenPlanSeatDetail } from "./types.ts";
import {
TOKEN_PLAN_AK_OPTIONS,
TOKEN_PLAN_COMMON_QUERY_OPTIONS,
appendCommonQueryParams,
callTokenPlanApi,
prepareTokenPlanRequest,
resolveTokenPlanCredentials,
type TokenPlanQueryParams,
} from "./utils.ts";
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" },
...TOKEN_PLAN_COMMON_QUERY_OPTIONS,
{
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",
},
...TOKEN_PLAN_AK_OPTIONS,
],
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 credentials = resolveTokenPlanCredentials(config, flags);
const queryParams = buildQueryParams(flags);
if (config.dryRun) {
const { endpoint, queryParams: query } = prepareTokenPlanRequest(
config,
API_PATH,
queryParams,
);
emitResult({ endpoint, query }, format);
return;
}
const data = await callTokenPlanApi<GetSubscriptionSeatDetailsResponse>({
config,
credentials,
action: API_ACTION,
path: API_PATH,
method: "GET",
queryParams,
});
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): TokenPlanQueryParams {
const params: TokenPlanQueryParams = {};
if (flags.pageNo !== undefined) params.PageNo = String(flags.pageNo as number);
if (flags.pageSize !== undefined) params.PageSize = String(flags.pageSize as number);
appendCommonQueryParams(params, flags);
if (flags.statusListStr) params.StatusListStr = flags.statusListStr as string;
const status = flags.status as string[] | undefined;
if (status && 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) {
const val = flags.queryAssigned.toLowerCase();
if (val !== "true" && val !== "false") {
throw new BailianError("--query-assigned must be 'true' or 'false'.", ExitCode.USAGE);
}
params.QueryAssigned = val;
}
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}` : ""}`,
);
}
}
@@ -0,0 +1,69 @@
// ---- 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;
};
}
export interface CreateTokenPlanKeyResponse {
Success?: boolean;
Code?: string;
Message?: string;
Data?: {
ApiKeyId?: string;
PlainApiKey?: string;
MaskedApiKey?: string;
Description?: string;
CreatedAt?: string;
SourceId?: string;
};
}
export interface BatchAssignSeatsResponse {
Success?: boolean;
Code?: string;
Message?: string;
}
export interface AddOrganizationMemberResponse {
Success?: boolean;
Code?: string;
Message?: string;
RequestId?: string;
HttpStatusCode?: number;
Data?: {
AccountId?: string;
SeatAssigned?: boolean;
};
}
@@ -0,0 +1,161 @@
import {
REGIONS,
maskToken,
trackingHeaders,
type Config,
type GlobalFlags,
type OptionDef,
type Region,
BailianError,
ExitCode,
} from "bailian-cli-core";
import { buildCanonicalQuery, signTokenPlanRequest } from "./ak-sign.ts";
export const TOKEN_PLAN_API_VERSION = "2026-02-10";
export const TOKEN_PLAN_AK_OPTIONS: OptionDef[] = [
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (deprecated)" },
{
flag: "--access-key-secret <key>",
description: "Alibaba Cloud Access Key Secret (deprecated)",
},
];
export const TOKEN_PLAN_COMMON_QUERY_OPTIONS: OptionDef[] = [
{
flag: "--caller-uac-account-id <id>",
description: "Caller UAC account ID",
},
{
flag: "--namespace-id <id>",
description: "Product namespace ID (Token Plan default: namespace-1)",
},
];
export const TOKEN_PLAN_WORKSPACE_OPTION: OptionDef = {
flag: "--workspace-id <id>",
description: "Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id)",
};
const MODEL_STUDIO_HOSTS: Partial<Record<Region, string>> = {
cn: "modelstudio.cn-beijing.aliyuncs.com",
intl: "modelstudio.ap-southeast-1.aliyuncs.com",
};
function resolveRegion(baseUrl: string): Region {
for (const [region, url] of Object.entries(REGIONS) as Array<[Region, string]>) {
if (baseUrl === url || baseUrl.startsWith(`${url}/`)) return region;
}
return "cn";
}
/** ModelStudio POP OpenAPI host for the given DashScope base URL preset. */
function modelStudioHost(baseUrl: string): string {
const region = resolveRegion(baseUrl);
return MODEL_STUDIO_HOSTS[region] ?? MODEL_STUDIO_HOSTS.cn!;
}
export interface TokenPlanApiResponse {
Success?: boolean;
Code?: string;
Message?: string;
}
export type TokenPlanQueryParams = Record<string, string | string[] | undefined>;
export function resolveTokenPlanCredentials(
config: Config,
flags: GlobalFlags,
): { accessKeyId: string; accessKeySecret: string } {
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,
);
}
return { accessKeyId, accessKeySecret };
}
export function requireWorkspaceId(config: Config, flags: GlobalFlags): string {
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
if (!workspaceId) {
throw new BailianError(
"Missing workspace ID.\n" +
"Set via: --workspace-id flag, env: BAILIAN_WORKSPACE_ID, or config: bl config set workspace_id <id>",
ExitCode.USAGE,
);
}
return workspaceId;
}
export function appendCommonQueryParams(params: TokenPlanQueryParams, flags: GlobalFlags): void {
if (flags.callerUacAccountId) params.CallerUacAccountId = flags.callerUacAccountId as string;
if (flags.namespaceId) params.NamespaceId = flags.namespaceId as string;
}
export function prepareTokenPlanRequest(
config: Config,
path: string,
queryParams: TokenPlanQueryParams,
): { host: string; endpoint: string; queryString: string; queryParams: TokenPlanQueryParams } {
const queryString = buildCanonicalQuery(queryParams);
const host = modelStudioHost(config.baseUrl);
const endpoint = `https://${host}${path}${queryString ? `?${queryString}` : ""}`;
return { host, endpoint, queryString, queryParams };
}
export async function callTokenPlanApi<T extends TokenPlanApiResponse>(opts: {
config: Config;
credentials: { accessKeyId: string; accessKeySecret: string };
action: string;
path: string;
method: "GET" | "POST";
queryParams: TokenPlanQueryParams;
}): Promise<T> {
const { config, credentials, action, path, method, queryParams } = opts;
const { host, endpoint, queryString } = prepareTokenPlanRequest(config, path, queryParams);
const headers = signTokenPlanRequest({
accessKeyId: credentials.accessKeyId,
accessKeySecret: credentials.accessKeySecret,
action,
version: TOKEN_PLAN_API_VERSION,
body: "",
host,
pathname: path,
method,
queryString,
});
if (config.verbose) {
process.stderr.write(`> ${method} ${endpoint}\n`);
process.stderr.write(`> AK: ${maskToken(credentials.accessKeyId)}\n`);
}
const timeoutMs = config.timeout * 1000;
const res = await fetch(endpoint, {
method,
headers: { ...headers, ...trackingHeaders() },
signal: AbortSignal.timeout(timeoutMs),
});
if (config.verbose) {
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
}
const data = (await res.json()) as T;
if (!res.ok || data.Success === false) {
throw new BailianError(
`${data.Code || res.status} - ${data.Message || res.statusText}`,
ExitCode.GENERAL,
);
}
return data;
}
+53 -48
View File
@@ -8,54 +8,58 @@ Use this index for the full quick index and global flags.
## Quick index
| Command | Description | Detail |
| -------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------- |
| `bl advisor recommend` | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) | [advisor.md](advisor.md) |
| `bl app call` | Call a Bailian application (agent or workflow) | [app.md](app.md) |
| `bl app list` | List Bailian applications | [app.md](app.md) |
| `bl auth login` | Authenticate with API key or console browser login (credentials can coexist) | [auth.md](auth.md) |
| `bl auth logout` | Clear stored credentials | [auth.md](auth.md) |
| `bl auth status` | Show current authentication state | [auth.md](auth.md) |
| `bl config export-schema` | Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas | [config.md](config.md) |
| `bl config set` | Set a config value | [config.md](config.md) |
| `bl config show` | Display current configuration | [config.md](config.md) |
| `bl console call` | Call a Bailian console API via the CLI gateway | [console.md](console.md) |
| `bl file upload` | Upload a local file to DashScope temporary storage (48h) | [file.md](file.md) |
| `bl image edit` | Edit an existing image with text instructions (Qwen-Image) | [image.md](image.md) |
| `bl image generate` | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) |
| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base | [knowledge.md](knowledge.md) |
| `bl mcp call` | Call a tool on an MCP server (tools/call) | [mcp.md](mcp.md) |
| `bl mcp list` | List MCP servers activated under your Bailian account | [mcp.md](mcp.md) |
| `bl mcp tools` | List tools exposed by an MCP server (tools/list) | [mcp.md](mcp.md) |
| `bl memory add` | Add memory from messages or custom content | [memory.md](memory.md) |
| `bl memory delete` | Delete a memory node | [memory.md](memory.md) |
| `bl memory list` | List memory nodes for a user | [memory.md](memory.md) |
| `bl memory profile create` | Create a user profile schema for memory profiling | [memory.md](memory.md) |
| `bl memory profile get` | Get user profile by schema ID and user ID | [memory.md](memory.md) |
| `bl memory search` | Search memory nodes by query or messages | [memory.md](memory.md) |
| `bl memory update` | Update a memory node content | [memory.md](memory.md) |
| `bl omni` | Multimodal chat with text + audio output (Qwen-Omni) | [omni.md](omni.md) |
| `bl pipeline run` | Run a pipeline workflow definition | [pipeline.md](pipeline.md) |
| `bl pipeline validate` | Validate a pipeline definition without executing | [pipeline.md](pipeline.md) |
| `bl quota check` | Check current usage against rate limits | [quota.md](quota.md) |
| `bl quota history` | View quota change history | [quota.md](quota.md) |
| `bl quota list` | View model RPM/TPM rate limits | [quota.md](quota.md) |
| `bl quota request` | Request a temporary quota increase | [quota.md](quota.md) |
| `bl search web` | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
| `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 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) |
| `bl usage stats` | Query model usage statistics | [usage.md](usage.md) |
| `bl video download` | Download a completed video by task ID | [video.md](video.md) |
| `bl video edit` | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) | [video.md](video.md) |
| `bl video generate` | Generate a video from text or image (happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v) | [video.md](video.md) |
| `bl video ref` | Reference-to-video generation (happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice | [video.md](video.md) |
| `bl video task get` | Query async task status | [video.md](video.md) |
| `bl vision describe` | Describe an image or video using Qwen-VL | [vision.md](vision.md) |
| `bl workspace list` | List all workspaces | [workspace.md](workspace.md) |
| Command | Description | Detail |
| --------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------- |
| `bl advisor recommend` | Recommend the best models for your use case (intent analysis → candidate recall → LLM ranking) | [advisor.md](advisor.md) |
| `bl app call` | Call a Bailian application (agent or workflow) | [app.md](app.md) |
| `bl app list` | List Bailian applications | [app.md](app.md) |
| `bl auth login` | Authenticate with API key or console browser login (credentials can coexist) | [auth.md](auth.md) |
| `bl auth logout` | Clear stored credentials | [auth.md](auth.md) |
| `bl auth status` | Show current authentication state | [auth.md](auth.md) |
| `bl config export-schema` | Export all (or one) CLI command(s) as Anthropic/OpenAI-compatible JSON tool schemas | [config.md](config.md) |
| `bl config set` | Set a config value | [config.md](config.md) |
| `bl config show` | Display current configuration | [config.md](config.md) |
| `bl console call` | Call a Bailian console API via the CLI gateway | [console.md](console.md) |
| `bl file upload` | Upload a local file to DashScope temporary storage (48h) | [file.md](file.md) |
| `bl image edit` | Edit an existing image with text instructions (Qwen-Image) | [image.md](image.md) |
| `bl image generate` | Generate images (Qwen-Image / wan2.x) | [image.md](image.md) |
| `bl knowledge retrieve` | Retrieve from a Bailian knowledge base | [knowledge.md](knowledge.md) |
| `bl mcp call` | Call a tool on an MCP server (tools/call) | [mcp.md](mcp.md) |
| `bl mcp list` | List MCP servers activated under your Bailian account | [mcp.md](mcp.md) |
| `bl mcp tools` | List tools exposed by an MCP server (tools/list) | [mcp.md](mcp.md) |
| `bl memory add` | Add memory from messages or custom content | [memory.md](memory.md) |
| `bl memory delete` | Delete a memory node | [memory.md](memory.md) |
| `bl memory list` | List memory nodes for a user | [memory.md](memory.md) |
| `bl memory profile create` | Create a user profile schema for memory profiling | [memory.md](memory.md) |
| `bl memory profile get` | Get user profile by schema ID and user ID | [memory.md](memory.md) |
| `bl memory search` | Search memory nodes by query or messages | [memory.md](memory.md) |
| `bl memory update` | Update a memory node content | [memory.md](memory.md) |
| `bl omni` | Multimodal chat with text + audio output (Qwen-Omni) | [omni.md](omni.md) |
| `bl pipeline run` | Run a pipeline workflow definition | [pipeline.md](pipeline.md) |
| `bl pipeline validate` | Validate a pipeline definition without executing | [pipeline.md](pipeline.md) |
| `bl quota check` | Check current usage against rate limits | [quota.md](quota.md) |
| `bl quota history` | View quota change history | [quota.md](quota.md) |
| `bl quota list` | View model RPM/TPM rate limits | [quota.md](quota.md) |
| `bl quota request` | Request a temporary quota increase | [quota.md](quota.md) |
| `bl search web` | Search the web using DashScope MCP WebSearch service | [search.md](search.md) |
| `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 add-member` | Add a member to a Token Plan organization | [tokenplan.md](tokenplan.md) |
| `bl tokenplan assign-seats` | Batch assign Token Plan seats to members | [tokenplan.md](tokenplan.md) |
| `bl tokenplan create-key` | Create a Token Plan API key for a seat | [tokenplan.md](tokenplan.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) |
| `bl usage stats` | Query model usage statistics | [usage.md](usage.md) |
| `bl video download` | Download a completed video by task ID | [video.md](video.md) |
| `bl video edit` | Edit a video with happyhorse-1.0-video-edit (style transfer, object replacement, etc.) | [video.md](video.md) |
| `bl video generate` | Generate a video from text or image (happyhorse-1.1-t2v / happyhorse-1.1-i2v / wan2.6-t2v) | [video.md](video.md) |
| `bl video ref` | Reference-to-video generation (happyhorse-1.1-r2v / wan2.6-r2v): multi-subject, multi-shot with voice | [video.md](video.md) |
| `bl video task get` | Query async task status | [video.md](video.md) |
| `bl vision describe` | Describe an image or video using Qwen-VL | [vision.md](vision.md) |
| `bl workspace list` | List all workspaces | [workspace.md](workspace.md) |
## By group
@@ -77,6 +81,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` | `add-member`, `assign-seats`, `create-key`, `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) |
+151
View File
@@ -0,0 +1,151 @@
# `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 add-member` | Add a member to a Token Plan organization |
| `bl tokenplan assign-seats` | Batch assign Token Plan seats to members |
| `bl tokenplan create-key` | Create a Token Plan API key for a seat |
| `bl tokenplan seats` | List Token Plan subscription seat details |
## Command details
### `bl tokenplan add-member`
| Field | Value |
| --------------- | --------------------------------------------------------------------- |
| **Name** | `tokenplan add-member` |
| **Description** | Add a member to a Token Plan organization |
| **Usage** | `bl tokenplan add-member --account-name <name> --org-id <id> [flags]` |
#### Options
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | ---------------------------------------------------------------- |
| `--account-name <name>` | string | yes | Member display name |
| `--org-id <id>` | string | yes | Organization ID |
| `--org-role-code <code>` | string | no | Organization role: ORG_ADMIN or ORG_MEMBER (default: ORG_MEMBER) |
| `--spec-type <type>` | string | no | Seat tier to assign on creation: standard, pro, or max |
| `--caller-uac-account-id <id>` | string | no | Caller UAC account ID |
| `--namespace-id <id>` | string | no | Product namespace ID (Token Plan default: namespace-1) |
| `--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 add-member --account-name dev_user --org-id org_123
```
```bash
bl tokenplan add-member --account-name admin_user --org-id org_123 --org-role-code ORG_ADMIN
```
```bash
bl tokenplan add-member --account-name member1 --org-id org_123 --spec-type standard
```
### `bl tokenplan assign-seats`
| Field | Value |
| --------------- | -------------------------------------------------------------------------------------------- |
| **Name** | `tokenplan assign-seats` |
| **Description** | Batch assign Token Plan seats to members |
| **Usage** | `bl tokenplan assign-seats --workspace-id <id> --seat-type <type> --account-id <id> [flags]` |
#### Options
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | -------------------------------------------------------------- |
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id) |
| `--seat-type <type>` | string | yes | Seat tier: standard, pro, or max |
| `--account-id <id>` | array | no | Target member account ID (repeatable) |
| `--caller-uac-account-id <id>` | string | no | Caller UAC account ID |
| `--namespace-id <id>` | string | no | Product namespace ID (Token Plan default: namespace-1) |
| `--locale <locale>` | string | no | Language: zh-CN or en-US |
| `--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 assign-seats --workspace-id ws_456 --seat-type standard --account-id acc_123
```
```bash
bl tokenplan assign-seats --workspace-id ws_456 --seat-type pro --account-id acc_1 --account-id acc_2
```
### `bl tokenplan create-key`
| Field | Value |
| --------------- | ----------------------------------------------------------------------- |
| **Name** | `tokenplan create-key` |
| **Description** | Create a Token Plan API key for a seat |
| **Usage** | `bl tokenplan create-key --account-id <id> --workspace-id <id> [flags]` |
#### Options
| Flag | Type | Required | Description |
| ------------------------------ | ------ | -------- | -------------------------------------------------------------- |
| `--account-id <id>` | string | yes | Target member account ID |
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id) |
| `--description <text>` | string | no | API key description |
| `--caller-uac-account-id <id>` | string | no | Caller UAC account ID |
| `--namespace-id <id>` | string | no | Product namespace ID (Token Plan default: namespace-1) |
| `--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 create-key --account-id acc_123 --workspace-id ws_456
```
```bash
bl tokenplan create-key --account-id acc_123 --workspace-id ws_456 --description 'Dev key'
```
### `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
```