feat(tokenplan): 添加 Token Plan 相关命令

新增 `tokenplan add-member`、`tokenplan assign-seats` 和 `tokenplan create-key` 命令,支持管理 Token Plan 组织成员和 API 密钥。相关文档已更新,提供使用示例和参数说明。
This commit is contained in:
wb-liuxuehuan
2026-06-23 16:51:54 +08:00
parent c0d30fee3d
commit d74686f09f
10 changed files with 996 additions and 71 deletions
+8 -2
View File
@@ -119,6 +119,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&)
@@ -148,9 +154,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。
+6
View File
@@ -47,6 +47,9 @@ 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> = {
@@ -96,5 +99,8 @@ export const commands: Record<string, Command> = {
"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,160 @@
import {
defineCommand,
buildCanonicalQuery,
signRequest,
modelStudioHost,
detectOutputFormat,
maskToken,
trackingHeaders,
type Config,
type GlobalFlags,
type AddOrganizationMemberResponse,
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 = "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",
},
{
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: "--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 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 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 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);
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: "POST",
queryString,
});
if (config.verbose) {
process.stderr.write(`> POST ${endpoint}\n`);
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
}
const timeoutMs = config.timeout * 1000;
const res = await fetch(endpoint, {
method: "POST",
headers: { ...headers, ...trackingHeaders() },
signal: AbortSignal.timeout(timeoutMs),
});
if (config.verbose) {
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
}
const data = (await res.json()) as AddOrganizationMemberResponse;
if (!res.ok || data.Success === false) {
throw new BailianError(
`${data.Code || res.status} - ${data.Message || res.statusText}`,
ExitCode.GENERAL,
);
}
if (config.quiet || format === "text") {
emitTextMember(data);
} else {
emitResult(data, format);
}
},
});
function buildQueryParams(flags: GlobalFlags): Record<string, string | string[] | undefined> {
const params: Record<string, string | string[] | undefined> = {};
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;
if (flags.callerUacAccountId) params.CallerUacAccountId = flags.callerUacAccountId as string;
if (flags.namespaceId) params.NamespaceId = flags.namespaceId as string;
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,172 @@
import {
defineCommand,
buildCanonicalQuery,
signRequest,
modelStudioHost,
detectOutputFormat,
maskToken,
trackingHeaders,
type Config,
type GlobalFlags,
type BatchAssignSeatsResponse,
BailianError,
ExitCode,
} from "bailian-cli-core";
import { emitResult, emitBare } from "../../output/output.ts";
const API_VERSION = "2026-02-10";
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: [
{
flag: "--workspace-id <id>",
description: "Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id)",
},
{
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",
},
{
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: "--locale <locale>",
description: "Language: zh-CN or en-US",
},
{ 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 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 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 workspaceId = (flags.workspaceId as string) || config.workspaceId;
const seatType = flags.seatType as string | undefined;
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,
);
}
if (!seatType) {
throw new BailianError("Missing required argument --seat-type.", ExitCode.USAGE);
}
const accountIds = flags.accountId;
const hasAccountIds =
(Array.isArray(accountIds) && accountIds.length > 0) ||
(typeof accountIds === "string" && accountIds.length > 0);
if (!hasAccountIds) {
throw new BailianError("Missing required argument --account-id.", ExitCode.USAGE);
}
const queryParams = buildQueryParams(flags, workspaceId);
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: "POST",
queryString,
});
if (config.verbose) {
process.stderr.write(`> POST ${endpoint}\n`);
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
}
const timeoutMs = config.timeout * 1000;
const res = await fetch(endpoint, {
method: "POST",
headers: { ...headers, ...trackingHeaders() },
signal: AbortSignal.timeout(timeoutMs),
});
if (config.verbose) {
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
}
const data = (await res.json()) as BatchAssignSeatsResponse;
if (!res.ok || data.Success === false) {
throw new BailianError(
`${data.Code || res.status} - ${data.Message || res.statusText}`,
ExitCode.GENERAL,
);
}
if (config.quiet || format === "text") {
emitBare("Seats assigned successfully.");
} else {
emitResult(data, format);
}
},
});
function buildQueryParams(
flags: GlobalFlags,
workspaceId: string,
): Record<string, string | string[] | undefined> {
const params: Record<string, string | string[] | undefined> = {};
params.WorkspaceId = workspaceId;
if (flags.seatType) params.SeatType = flags.seatType as string;
if (flags.callerUacAccountId) params.CallerUacAccountId = flags.callerUacAccountId as string;
if (flags.namespaceId) params.NamespaceId = flags.namespaceId as string;
if (flags.locale) params.Locale = flags.locale as string;
const accountIds = flags.accountId as string | string[] | undefined;
if (Array.isArray(accountIds) && accountIds.length > 0) {
params.AccountIds = accountIds;
} else if (typeof accountIds === "string" && accountIds.length > 0) {
params.AccountIds = accountIds;
}
return params;
}
@@ -0,0 +1,163 @@
import {
defineCommand,
buildCanonicalQuery,
signRequest,
modelStudioHost,
detectOutputFormat,
maskToken,
trackingHeaders,
type Config,
type GlobalFlags,
type CreateTokenPlanKeyResponse,
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 = "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 },
{
flag: "--workspace-id <id>",
description: "Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id)",
},
{ flag: "--description <text>", description: "API key description" },
{
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: "--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 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 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 accountId = flags.accountId as string | undefined;
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
if (!accountId) {
throw new BailianError("Missing required argument --account-id.", ExitCode.USAGE);
}
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,
);
}
const queryParams = buildQueryParams(flags, { accountId, workspaceId });
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: "POST",
queryString,
});
if (config.verbose) {
process.stderr.write(`> POST ${endpoint}\n`);
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
}
const timeoutMs = config.timeout * 1000;
const res = await fetch(endpoint, {
method: "POST",
headers: { ...headers, ...trackingHeaders() },
signal: AbortSignal.timeout(timeoutMs),
});
if (config.verbose) {
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
}
const data = (await res.json()) as CreateTokenPlanKeyResponse;
if (!res.ok || data.Success === false) {
throw new BailianError(
`${data.Code || res.status} - ${data.Message || res.statusText}`,
ExitCode.GENERAL,
);
}
if (config.quiet || format === "text") {
emitTextKey(data);
} else {
emitResult(data, format);
}
},
});
function buildQueryParams(
flags: GlobalFlags,
resolved: { accountId: string; workspaceId: string },
): Record<string, string | string[] | undefined> {
const params: Record<string, string | string[] | undefined> = {};
params.AccountId = resolved.accountId;
params.WorkspaceId = resolved.workspaceId;
if (flags.description) params.Description = flags.description as string;
if (flags.callerUacAccountId) params.CallerUacAccountId = flags.callerUacAccountId as string;
if (flags.namespaceId) params.NamespaceId = flags.namespaceId as string;
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}`);
}
}
+3
View File
@@ -71,6 +71,9 @@ const NO_AUTH_SETUP = [
["quota", "history"],
["quota", "check"],
["tokenplan", "seats"],
["tokenplan", "create-key"],
["tokenplan", "assign-seats"],
["tokenplan", "add-member"],
];
async function main() {
+297 -16
View File
@@ -7,12 +7,28 @@ interface DryRunBody {
query?: Record<string, unknown>;
}
describe("e2e: tokenplan seats", () => {
const noCredsEnv = {
DASHSCOPE_API_KEY: undefined,
DASHSCOPE_ACCESS_TOKEN: undefined,
ALIBABA_CLOUD_ACCESS_KEY_ID: undefined,
ALIBABA_CLOUD_ACCESS_KEY_SECRET: undefined,
BAILIAN_CONFIG_DIR: tmpdir(),
};
const fakeAkEnv = {
ALIBABA_CLOUD_ACCESS_KEY_ID: "LTAI-fake",
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "fake-secret",
};
describe("e2e: tokenplan", () => {
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);
expect(out).toMatch(/create-key/i);
expect(out).toMatch(/assign-seats/i);
expect(out).toMatch(/add-member/i);
});
test("tokenplan seats --help 正常退出", async () => {
@@ -24,27 +40,195 @@ describe("e2e: tokenplan seats", () => {
expect(stderr).toMatch(/--status/i);
expect(stderr).toMatch(/--query-assigned/i);
});
test("tokenplan create-key --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["tokenplan", "create-key", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--account-id/i);
expect(stderr).toMatch(/--workspace-id/i);
});
test("tokenplan assign-seats --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["tokenplan", "assign-seats", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--workspace-id/i);
expect(stderr).toMatch(/--seat-type/i);
expect(stderr).toMatch(/--account-id/i);
});
test("tokenplan add-member --help 正常退出", async () => {
const { stderr, exitCode } = await runCli(["tokenplan", "add-member", "--help"]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--account-name/i);
expect(stderr).toMatch(/--org-id/i);
expect(stderr).toMatch(/--org-role-code/i);
});
});
describe("e2e: tokenplan seats errors", () => {
test("无任何凭证时提示 No credentials found 并非零退出", async () => {
describe("e2e: tokenplan errors", () => {
test("seats 无任何凭证时提示 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(),
},
noCredsEnv,
);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/no credentials found/i);
});
test("create-key 无任何凭证时提示 No credentials found 并非零退出", async () => {
const { stderr, exitCode } = await runCli(
[
"tokenplan",
"create-key",
"--account-id",
"acc_1",
"--workspace-id",
"ws_1",
"--non-interactive",
"--output",
"json",
],
noCredsEnv,
);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/no credentials found/i);
});
test("assign-seats 无任何凭证时提示 No credentials found 并非零退出", async () => {
const { stderr, exitCode } = await runCli(
[
"tokenplan",
"assign-seats",
"--workspace-id",
"ws_1",
"--seat-type",
"standard",
"--account-id",
"acc_1",
"--non-interactive",
"--output",
"json",
],
noCredsEnv,
);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/no credentials found/i);
});
test("add-member 无任何凭证时提示 No credentials found 并非零退出", async () => {
const { stderr, exitCode } = await runCli(
[
"tokenplan",
"add-member",
"--account-name",
"user1",
"--org-id",
"org_1",
"--non-interactive",
"--output",
"json",
],
noCredsEnv,
);
expect(exitCode).not.toBe(0);
expect(stderr).toMatch(/no credentials found/i);
});
});
describe("e2e: tokenplan seats dry-run", () => {
test("--dry-run 输出 endpoint 和 query 参数", async () => {
describe.skipIf(!isTokenPlanAkSkReady())("e2e: tokenplan missing args", () => {
test("create-key 缺少 --account-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli([
"tokenplan",
"create-key",
"--workspace-id",
"ws_1",
"--non-interactive",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--account-id|Missing required argument/i);
});
test("create-key 缺少 --workspace-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli([
"tokenplan",
"create-key",
"--account-id",
"acc_1",
"--non-interactive",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/workspace-id|Missing workspace ID/i);
});
test("assign-seats 缺少 --workspace-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli([
"tokenplan",
"assign-seats",
"--seat-type",
"standard",
"--account-id",
"acc_1",
"--non-interactive",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/workspace-id|Missing workspace ID/i);
});
test("assign-seats 缺少 --seat-type 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli([
"tokenplan",
"assign-seats",
"--workspace-id",
"ws_1",
"--account-id",
"acc_1",
"--non-interactive",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--seat-type|Missing required argument/i);
});
test("assign-seats 缺少 account id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli([
"tokenplan",
"assign-seats",
"--workspace-id",
"ws_1",
"--seat-type",
"standard",
"--non-interactive",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--account-id|Missing required argument/i);
});
test("add-member 缺少 --account-name 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli([
"tokenplan",
"add-member",
"--org-id",
"org_1",
"--non-interactive",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--account-name|Missing required argument/i);
});
test("add-member 缺少 --org-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCli([
"tokenplan",
"add-member",
"--account-name",
"user1",
"--non-interactive",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--org-id|Missing required argument/i);
});
});
describe("e2e: tokenplan dry-run", () => {
test("seats --dry-run 输出 endpoint 和 query 参数", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"tokenplan",
@@ -62,10 +246,7 @@ describe("e2e: tokenplan seats dry-run", () => {
"--output",
"json",
],
{
ALIBABA_CLOUD_ACCESS_KEY_ID: "LTAI-fake",
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "fake-secret",
},
fakeAkEnv,
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
@@ -75,6 +256,106 @@ describe("e2e: tokenplan seats dry-run", () => {
expect(data.query?.QueryAssigned).toBe("true");
expect(data.query?.StatusList).toEqual(["NORMAL"]);
});
test("create-key --dry-run 从 BAILIAN_WORKSPACE_ID 读取 workspace", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"tokenplan",
"create-key",
"--dry-run",
"--account-id",
"acc_123",
"--non-interactive",
"--output",
"json",
],
{ ...fakeAkEnv, BAILIAN_WORKSPACE_ID: "ws-from-env" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.query?.WorkspaceId).toBe("ws-from-env");
});
test("create-key --dry-run 输出 endpoint 和 query 参数", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"tokenplan",
"create-key",
"--dry-run",
"--account-id",
"acc_123",
"--workspace-id",
"ws_456",
"--description",
"test key",
"--non-interactive",
"--output",
"json",
],
fakeAkEnv,
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.endpoint).toMatch(/\/tokenplan\/api-keys/);
expect(data.query?.AccountId).toBe("acc_123");
expect(data.query?.WorkspaceId).toBe("ws_456");
expect(data.query?.Description).toBe("test key");
});
test("assign-seats --dry-run 输出 endpoint 和 query 参数", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"tokenplan",
"assign-seats",
"--dry-run",
"--workspace-id",
"ws_456",
"--seat-type",
"standard",
"--account-id",
"acc_1",
"--account-id",
"acc_2",
"--non-interactive",
"--output",
"json",
],
fakeAkEnv,
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.endpoint).toMatch(/\/tokenplan\/subscription\/seat-assignments/);
expect(data.query?.WorkspaceId).toBe("ws_456");
expect(data.query?.SeatType).toBe("standard");
expect(data.query?.AccountIds).toEqual(["acc_1", "acc_2"]);
});
test("add-member --dry-run 输出 endpoint 和 query 参数", async () => {
const { stdout, stderr, exitCode } = await runCli(
[
"tokenplan",
"add-member",
"--dry-run",
"--account-name",
"dev_user",
"--org-id",
"org_123",
"--spec-type",
"standard",
"--non-interactive",
"--output",
"json",
],
fakeAkEnv,
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<DryRunBody>(stdout);
expect(data.endpoint).toMatch(/\/tokenplan\/organization\/member-additions/);
expect(data.query?.AccountName).toBe("dev_user");
expect(data.query?.OrgId).toBe("org_123");
expect(data.query?.OrgRoleCode).toBe("ORG_MEMBER");
expect(data.query?.SpecType).toBe("standard");
});
});
describe.skipIf(!isTokenPlanAkSkReady())("e2e: tokenplan seats(AK/SK)", () => {
+32
View File
@@ -455,6 +455,38 @@ export interface GetSubscriptionSeatDetailsResponse {
};
}
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;
};
}
// ---- Speech Synthesis / TTS (DashScope) ----
export interface DashScopeTTSRequest {
+53 -50
View File
@@ -8,55 +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 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.0-t2v / happyhorse-1.0-i2v / wan2.6-t2v) | [video.md](video.md) |
| `bl video ref` | Reference-to-video generation (happyhorse-1.0-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.0-t2v / happyhorse-1.0-i2v / wan2.6-t2v) | [video.md](video.md) |
| `bl video ref` | Reference-to-video generation (happyhorse-1.0-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
@@ -78,7 +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` | `seats` | [tokenplan.md](tokenplan.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) |
+102 -3
View File
@@ -7,12 +7,111 @@ Index: [index.md](index.md)
## Commands in this group
| Command | Description |
| -------------------- | ----------------------------------------- |
| `bl tokenplan seats` | List Token Plan subscription seat details |
| 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 |