Skip to content

Commit d74686f

Browse files
author
wb-liuxuehuan
committed
feat(tokenplan): 添加 Token Plan 相关命令
新增 `tokenplan add-member`、`tokenplan assign-seats` 和 `tokenplan create-key` 命令,支持管理 Token Plan 组织成员和 API 密钥。相关文档已更新,提供使用示例和参数说明。
1 parent c0d30fe commit d74686f

10 files changed

Lines changed: 996 additions & 71 deletions

File tree

packages/cli/README.zh.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ bl quota check # 查看当前用量 vs
119119
bl quota check --model qwen3.6-plus --period 5 # 查看最近 5 分钟用量
120120
bl quota request --model qwen3.6-plus --tpm 6000000 # 申请临时 TPM 提额
121121
bl quota history # 查看提额历史记录
122+
123+
# Token Plan 团队版管理(需 AK/SK,见下方认证说明)
124+
bl tokenplan seats # 查看订阅席位明细
125+
bl tokenplan add-member --account-name dev --org-id org_xxx
126+
bl tokenplan assign-seats --workspace-id ws_xxx --seat-type standard --account-id acc_xxx
127+
bl tokenplan create-key --account-id acc_xxx --workspace-id ws_xxx
122128
```
123129

124130
> 更多案例与使用场景:[阿里云百炼 CLI 官方主页](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
@@ -148,9 +154,9 @@ bl text chat --api-key sk-xxxxx --message "你好"
148154
bl auth login --console
149155
```
150156

151-
### 阿里云 AK/SK(仅知识库检索
157+
### 阿里云 AK/SK(知识库检索与 Token Plan
152158

153-
`knowledge retrieve` 命令需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
159+
`knowledge retrieve` `tokenplan` 命令组需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
154160

155161
> 建议:创建 RAM 子账号并授予最小权限,避免使用主账号 AK/SK。
156162

packages/cli/src/commands/catalog.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ import quotaRequest from "./quota/request.ts";
4747
import quotaHistory from "./quota/history.ts";
4848
import quotaCheck from "./quota/check.ts";
4949
import tokenplanSeats from "./tokenplan/seats.ts";
50+
import tokenplanCreateKey from "./tokenplan/create-key.ts";
51+
import tokenplanAssignSeats from "./tokenplan/assign-seats.ts";
52+
import tokenplanAddMember from "./tokenplan/add-member.ts";
5053

5154
/** Command registry map (no dependency on registry.ts — safe for build-time import). */
5255
export const commands: Record<string, Command> = {
@@ -96,5 +99,8 @@ export const commands: Record<string, Command> = {
9699
"quota history": quotaHistory,
97100
"quota check": quotaCheck,
98101
"tokenplan seats": tokenplanSeats,
102+
"tokenplan create-key": tokenplanCreateKey,
103+
"tokenplan assign-seats": tokenplanAssignSeats,
104+
"tokenplan add-member": tokenplanAddMember,
99105
update: update,
100106
};
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import {
2+
defineCommand,
3+
buildCanonicalQuery,
4+
signRequest,
5+
modelStudioHost,
6+
detectOutputFormat,
7+
maskToken,
8+
trackingHeaders,
9+
type Config,
10+
type GlobalFlags,
11+
type AddOrganizationMemberResponse,
12+
BailianError,
13+
ExitCode,
14+
} from "bailian-cli-core";
15+
import { emitResult, emitBare } from "../../output/output.ts";
16+
import { padEnd } from "../../output/cjk-width.ts";
17+
18+
const API_VERSION = "2026-02-10";
19+
const API_ACTION = "AddOrganizationMember";
20+
const API_PATH = "/tokenplan/organization/member-additions";
21+
22+
const DEFAULT_ORG_ROLE = "ORG_MEMBER";
23+
24+
export default defineCommand({
25+
name: "tokenplan add-member",
26+
description: "Add a member to a Token Plan organization",
27+
usage: "bl tokenplan add-member --account-name <name> --org-id <id> [flags]",
28+
options: [
29+
{ flag: "--account-name <name>", description: "Member display name", required: true },
30+
{ flag: "--org-id <id>", description: "Organization ID", required: true },
31+
{
32+
flag: "--org-role-code <code>",
33+
description: "Organization role: ORG_ADMIN or ORG_MEMBER (default: ORG_MEMBER)",
34+
},
35+
{
36+
flag: "--spec-type <type>",
37+
description: "Seat tier to assign on creation: standard, pro, or max",
38+
},
39+
{
40+
flag: "--caller-uac-account-id <id>",
41+
description: "Caller UAC account ID",
42+
},
43+
{
44+
flag: "--namespace-id <id>",
45+
description: "Product namespace ID (Token Plan default: namespace-1)",
46+
},
47+
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (deprecated)" },
48+
{
49+
flag: "--access-key-secret <key>",
50+
description: "Alibaba Cloud Access Key Secret (deprecated)",
51+
},
52+
],
53+
examples: [
54+
"bl tokenplan add-member --account-name dev_user --org-id org_123",
55+
"bl tokenplan add-member --account-name admin_user --org-id org_123 --org-role-code ORG_ADMIN",
56+
"bl tokenplan add-member --account-name member1 --org-id org_123 --spec-type standard",
57+
],
58+
async run(config: Config, flags: GlobalFlags) {
59+
const format = detectOutputFormat(config.output);
60+
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
61+
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
62+
63+
if (!accessKeyId || !accessKeySecret) {
64+
throw new BailianError(
65+
"No credentials found.\n" +
66+
"Set ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET.",
67+
ExitCode.AUTH,
68+
);
69+
}
70+
71+
const accountName = flags.accountName as string | undefined;
72+
const orgId = flags.orgId as string | undefined;
73+
if (!accountName) {
74+
throw new BailianError("Missing required argument --account-name.", ExitCode.USAGE);
75+
}
76+
if (!orgId) {
77+
throw new BailianError("Missing required argument --org-id.", ExitCode.USAGE);
78+
}
79+
80+
const queryParams = buildQueryParams(flags);
81+
const queryString = buildCanonicalQuery(queryParams);
82+
const host = modelStudioHost(config.region);
83+
const endpoint = `https://${host}${API_PATH}${queryString ? `?${queryString}` : ""}`;
84+
85+
if (config.dryRun) {
86+
emitResult({ endpoint, query: queryParams }, format);
87+
return;
88+
}
89+
90+
const headers = signRequest({
91+
accessKeyId,
92+
accessKeySecret,
93+
action: API_ACTION,
94+
version: API_VERSION,
95+
body: "",
96+
host,
97+
pathname: API_PATH,
98+
method: "POST",
99+
queryString,
100+
});
101+
102+
if (config.verbose) {
103+
process.stderr.write(`> POST ${endpoint}\n`);
104+
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
105+
}
106+
107+
const timeoutMs = config.timeout * 1000;
108+
const res = await fetch(endpoint, {
109+
method: "POST",
110+
headers: { ...headers, ...trackingHeaders() },
111+
signal: AbortSignal.timeout(timeoutMs),
112+
});
113+
114+
if (config.verbose) {
115+
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
116+
}
117+
118+
const data = (await res.json()) as AddOrganizationMemberResponse;
119+
120+
if (!res.ok || data.Success === false) {
121+
throw new BailianError(
122+
`${data.Code || res.status} - ${data.Message || res.statusText}`,
123+
ExitCode.GENERAL,
124+
);
125+
}
126+
127+
if (config.quiet || format === "text") {
128+
emitTextMember(data);
129+
} else {
130+
emitResult(data, format);
131+
}
132+
},
133+
});
134+
135+
function buildQueryParams(flags: GlobalFlags): Record<string, string | string[] | undefined> {
136+
const params: Record<string, string | string[] | undefined> = {};
137+
138+
if (flags.accountName) params.AccountName = flags.accountName as string;
139+
if (flags.orgId) params.OrgId = flags.orgId as string;
140+
params.OrgRoleCode =
141+
typeof flags.orgRoleCode === "string" && flags.orgRoleCode.length > 0
142+
? flags.orgRoleCode
143+
: DEFAULT_ORG_ROLE;
144+
if (flags.specType) params.SpecType = flags.specType as string;
145+
if (flags.callerUacAccountId) params.CallerUacAccountId = flags.callerUacAccountId as string;
146+
if (flags.namespaceId) params.NamespaceId = flags.namespaceId as string;
147+
148+
return params;
149+
}
150+
151+
function emitTextMember(data: AddOrganizationMemberResponse): void {
152+
const item = data.Data;
153+
if (!item) {
154+
emitBare("Member added.");
155+
return;
156+
}
157+
158+
emitBare(`${padEnd("AccountId", 14)} ${item.AccountId ?? "-"}`);
159+
emitBare(`${padEnd("SeatAssigned", 14)} ${String(item.SeatAssigned ?? "-")}`);
160+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {
2+
defineCommand,
3+
buildCanonicalQuery,
4+
signRequest,
5+
modelStudioHost,
6+
detectOutputFormat,
7+
maskToken,
8+
trackingHeaders,
9+
type Config,
10+
type GlobalFlags,
11+
type BatchAssignSeatsResponse,
12+
BailianError,
13+
ExitCode,
14+
} from "bailian-cli-core";
15+
import { emitResult, emitBare } from "../../output/output.ts";
16+
17+
const API_VERSION = "2026-02-10";
18+
const API_ACTION = "BatchAssignSeats";
19+
const API_PATH = "/tokenplan/subscription/seat-assignments";
20+
21+
export default defineCommand({
22+
name: "tokenplan assign-seats",
23+
description: "Batch assign Token Plan seats to members",
24+
usage:
25+
"bl tokenplan assign-seats --workspace-id <id> --seat-type <type> --account-id <id> [flags]",
26+
options: [
27+
{
28+
flag: "--workspace-id <id>",
29+
description: "Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id)",
30+
},
31+
{
32+
flag: "--seat-type <type>",
33+
description: "Seat tier: standard, pro, or max",
34+
required: true,
35+
},
36+
{
37+
flag: "--account-id <id>",
38+
description: "Target member account ID (repeatable)",
39+
type: "array",
40+
},
41+
{
42+
flag: "--caller-uac-account-id <id>",
43+
description: "Caller UAC account ID",
44+
},
45+
{
46+
flag: "--namespace-id <id>",
47+
description: "Product namespace ID (Token Plan default: namespace-1)",
48+
},
49+
{
50+
flag: "--locale <locale>",
51+
description: "Language: zh-CN or en-US",
52+
},
53+
{ flag: "--access-key-id <key>", description: "Alibaba Cloud Access Key ID (deprecated)" },
54+
{
55+
flag: "--access-key-secret <key>",
56+
description: "Alibaba Cloud Access Key Secret (deprecated)",
57+
},
58+
],
59+
examples: [
60+
"bl tokenplan assign-seats --workspace-id ws_456 --seat-type standard --account-id acc_123",
61+
"bl tokenplan assign-seats --workspace-id ws_456 --seat-type pro --account-id acc_1 --account-id acc_2",
62+
],
63+
async run(config: Config, flags: GlobalFlags) {
64+
const format = detectOutputFormat(config.output);
65+
const accessKeyId = (flags.accessKeyId as string) || config.accessKeyId;
66+
const accessKeySecret = (flags.accessKeySecret as string) || config.accessKeySecret;
67+
68+
if (!accessKeyId || !accessKeySecret) {
69+
throw new BailianError(
70+
"No credentials found.\n" +
71+
"Set ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET.",
72+
ExitCode.AUTH,
73+
);
74+
}
75+
76+
const workspaceId = (flags.workspaceId as string) || config.workspaceId;
77+
const seatType = flags.seatType as string | undefined;
78+
if (!workspaceId) {
79+
throw new BailianError(
80+
"Missing workspace ID.\n" +
81+
"Set via: --workspace-id flag, env: BAILIAN_WORKSPACE_ID, or config: bl config set workspace_id <id>",
82+
ExitCode.USAGE,
83+
);
84+
}
85+
if (!seatType) {
86+
throw new BailianError("Missing required argument --seat-type.", ExitCode.USAGE);
87+
}
88+
89+
const accountIds = flags.accountId;
90+
const hasAccountIds =
91+
(Array.isArray(accountIds) && accountIds.length > 0) ||
92+
(typeof accountIds === "string" && accountIds.length > 0);
93+
if (!hasAccountIds) {
94+
throw new BailianError("Missing required argument --account-id.", ExitCode.USAGE);
95+
}
96+
97+
const queryParams = buildQueryParams(flags, workspaceId);
98+
const queryString = buildCanonicalQuery(queryParams);
99+
const host = modelStudioHost(config.region);
100+
const endpoint = `https://${host}${API_PATH}${queryString ? `?${queryString}` : ""}`;
101+
102+
if (config.dryRun) {
103+
emitResult({ endpoint, query: queryParams }, format);
104+
return;
105+
}
106+
107+
const headers = signRequest({
108+
accessKeyId,
109+
accessKeySecret,
110+
action: API_ACTION,
111+
version: API_VERSION,
112+
body: "",
113+
host,
114+
pathname: API_PATH,
115+
method: "POST",
116+
queryString,
117+
});
118+
119+
if (config.verbose) {
120+
process.stderr.write(`> POST ${endpoint}\n`);
121+
process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`);
122+
}
123+
124+
const timeoutMs = config.timeout * 1000;
125+
const res = await fetch(endpoint, {
126+
method: "POST",
127+
headers: { ...headers, ...trackingHeaders() },
128+
signal: AbortSignal.timeout(timeoutMs),
129+
});
130+
131+
if (config.verbose) {
132+
process.stderr.write(`< ${res.status} ${res.statusText}\n`);
133+
}
134+
135+
const data = (await res.json()) as BatchAssignSeatsResponse;
136+
137+
if (!res.ok || data.Success === false) {
138+
throw new BailianError(
139+
`${data.Code || res.status} - ${data.Message || res.statusText}`,
140+
ExitCode.GENERAL,
141+
);
142+
}
143+
144+
if (config.quiet || format === "text") {
145+
emitBare("Seats assigned successfully.");
146+
} else {
147+
emitResult(data, format);
148+
}
149+
},
150+
});
151+
152+
function buildQueryParams(
153+
flags: GlobalFlags,
154+
workspaceId: string,
155+
): Record<string, string | string[] | undefined> {
156+
const params: Record<string, string | string[] | undefined> = {};
157+
158+
params.WorkspaceId = workspaceId;
159+
if (flags.seatType) params.SeatType = flags.seatType as string;
160+
if (flags.callerUacAccountId) params.CallerUacAccountId = flags.callerUacAccountId as string;
161+
if (flags.namespaceId) params.NamespaceId = flags.namespaceId as string;
162+
if (flags.locale) params.Locale = flags.locale as string;
163+
164+
const accountIds = flags.accountId as string | string[] | undefined;
165+
if (Array.isArray(accountIds) && accountIds.length > 0) {
166+
params.AccountIds = accountIds;
167+
} else if (typeof accountIds === "string" && accountIds.length > 0) {
168+
params.AccountIds = accountIds;
169+
}
170+
171+
return params;
172+
}

0 commit comments

Comments
 (0)