From ba1661356f29a10f9ac3039334ec39550cf9dfc2 Mon Sep 17 00:00:00 2001 From: wb-liuxuehuan Date: Wed, 24 Jun 2026 11:14:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(tokenplan):=20=E9=87=8D=E6=9E=84=20Token?= =?UTF-8?q?=20Plan=20=E5=91=BD=E4=BB=A4=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对 `tokenplan` 相关命令进行了重构,新增了 `ak-sign` 模块以支持 ACS3-HMAC-SHA256 签名,优化了参数处理逻辑,简化了对凭证的处理。更新了 `add-member`、`assign-seats`、`create-key` 和 `seats` 命令,增强了对参数的验证和处理,确保代码的可读性和健壮性。同时,新增了类型定义和工具函数以支持更好的代码结构。 --- .../cli/src/commands/tokenplan/add-member.ts | 98 ++--- .../cli/src/commands/tokenplan/ak-sign.ts | 103 +++++ .../src/commands/tokenplan/assign-seats.ts | 117 ++---- .../cli/src/commands/tokenplan/create-key.ts | 114 ++---- packages/cli/src/commands/tokenplan/seats.ts | 100 ++--- packages/cli/src/commands/tokenplan/types.ts | 69 ++++ packages/cli/src/commands/tokenplan/utils.ts | 161 ++++++++ packages/cli/tests/e2e/helpers.ts | 9 - packages/cli/tests/e2e/tokenplan.e2e.test.ts | 379 ------------------ packages/core/src/client/ak-sign.ts | 31 +- packages/core/src/client/endpoints.ts | 13 - packages/core/src/client/index.ts | 3 +- packages/core/src/types/api.ts | 70 ---- packages/core/tests/ak-sign.test.ts | 20 - 14 files changed, 451 insertions(+), 836 deletions(-) create mode 100644 packages/cli/src/commands/tokenplan/ak-sign.ts create mode 100644 packages/cli/src/commands/tokenplan/types.ts create mode 100644 packages/cli/src/commands/tokenplan/utils.ts delete mode 100644 packages/cli/tests/e2e/tokenplan.e2e.test.ts delete mode 100644 packages/core/tests/ak-sign.test.ts diff --git a/packages/cli/src/commands/tokenplan/add-member.ts b/packages/cli/src/commands/tokenplan/add-member.ts index f9c950e..4fb7fa5 100644 --- a/packages/cli/src/commands/tokenplan/add-member.ts +++ b/packages/cli/src/commands/tokenplan/add-member.ts @@ -1,21 +1,24 @@ 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"; +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_VERSION = "2026-02-10"; const API_ACTION = "AddOrganizationMember"; const API_PATH = "/tokenplan/organization/member-additions"; @@ -36,19 +39,8 @@ export default defineCommand({ flag: "--spec-type ", description: "Seat tier to assign on creation: standard, pro, or max", }, - { - flag: "--caller-uac-account-id ", - description: "Caller UAC account ID", - }, - { - flag: "--namespace-id ", - description: "Product namespace ID (Token Plan default: namespace-1)", - }, - { flag: "--access-key-id ", description: "Alibaba Cloud Access Key ID (deprecated)" }, - { - flag: "--access-key-secret ", - description: "Alibaba Cloud Access Key Secret (deprecated)", - }, + ...TOKEN_PLAN_COMMON_QUERY_OPTIONS, + ...TOKEN_PLAN_AK_OPTIONS, ], examples: [ "bl tokenplan add-member --account-name dev_user --org-id org_123", @@ -57,16 +49,7 @@ export default defineCommand({ ], 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 credentials = resolveTokenPlanCredentials(config, flags); const accountName = flags.accountName as string | undefined; const orgId = flags.orgId as string | undefined; @@ -78,52 +61,26 @@ export default defineCommand({ } 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); + const { endpoint, queryParams: query } = prepareTokenPlanRequest( + config, + API_PATH, + queryParams, + ); + emitResult({ endpoint, query }, format); return; } - const headers = signRequest({ - accessKeyId, - accessKeySecret, + const data = await callTokenPlanApi({ + config, + credentials, action: API_ACTION, - version: API_VERSION, - body: "", - host, - pathname: API_PATH, + path: API_PATH, method: "POST", - queryString, + queryParams, }); - 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 { @@ -132,8 +89,8 @@ export default defineCommand({ }, }); -function buildQueryParams(flags: GlobalFlags): Record { - const params: Record = {}; +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; @@ -142,8 +99,7 @@ function buildQueryParams(flags: GlobalFlags): Record): 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 { + 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 = { + 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"); +} diff --git a/packages/cli/src/commands/tokenplan/assign-seats.ts b/packages/cli/src/commands/tokenplan/assign-seats.ts index 9d04759..96da7ad 100644 --- a/packages/cli/src/commands/tokenplan/assign-seats.ts +++ b/packages/cli/src/commands/tokenplan/assign-seats.ts @@ -1,20 +1,25 @@ 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"; +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_VERSION = "2026-02-10"; const API_ACTION = "BatchAssignSeats"; const API_PATH = "/tokenplan/subscription/seat-assignments"; @@ -24,10 +29,7 @@ export default defineCommand({ usage: "bl tokenplan assign-seats --workspace-id --seat-type --account-id [flags]", options: [ - { - flag: "--workspace-id ", - description: "Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id)", - }, + TOKEN_PLAN_WORKSPACE_OPTION, { flag: "--seat-type ", description: "Seat tier: standard, pro, or max", @@ -38,23 +40,12 @@ export default defineCommand({ description: "Target member account ID (repeatable)", type: "array", }, - { - flag: "--caller-uac-account-id ", - description: "Caller UAC account ID", - }, - { - flag: "--namespace-id ", - description: "Product namespace ID (Token Plan default: namespace-1)", - }, + ...TOKEN_PLAN_COMMON_QUERY_OPTIONS, { flag: "--locale ", description: "Language: zh-CN or en-US", }, - { flag: "--access-key-id ", description: "Alibaba Cloud Access Key ID (deprecated)" }, - { - flag: "--access-key-secret ", - description: "Alibaba Cloud Access Key Secret (deprecated)", - }, + ...TOKEN_PLAN_AK_OPTIONS, ], examples: [ "bl tokenplan assign-seats --workspace-id ws_456 --seat-type standard --account-id acc_123", @@ -62,26 +53,10 @@ export default defineCommand({ ], 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; + const credentials = resolveTokenPlanCredentials(config, flags); - 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 workspaceId = requireWorkspaceId(config, flags); 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 ", - ExitCode.USAGE, - ); - } if (!seatType) { throw new BailianError("Missing required argument --seat-type.", ExitCode.USAGE); } @@ -92,52 +67,26 @@ export default defineCommand({ } 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); + const { endpoint, queryParams: query } = prepareTokenPlanRequest( + config, + API_PATH, + queryParams, + ); + emitResult({ endpoint, query }, format); return; } - const headers = signRequest({ - accessKeyId, - accessKeySecret, + const data = await callTokenPlanApi({ + config, + credentials, action: API_ACTION, - version: API_VERSION, - body: "", - host, - pathname: API_PATH, + path: API_PATH, method: "POST", - queryString, + queryParams, }); - 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 { @@ -146,16 +95,12 @@ export default defineCommand({ }, }); -function buildQueryParams( - flags: GlobalFlags, - workspaceId: string, -): Record { - const params: Record = {}; +function buildQueryParams(flags: GlobalFlags, workspaceId: string): TokenPlanQueryParams { + const params: TokenPlanQueryParams = {}; 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; + appendCommonQueryParams(params, flags); if (flags.locale) params.Locale = flags.locale as string; const accountIds = flags.accountId as string[] | undefined; diff --git a/packages/cli/src/commands/tokenplan/create-key.ts b/packages/cli/src/commands/tokenplan/create-key.ts index e2fe431..00e19f9 100644 --- a/packages/cli/src/commands/tokenplan/create-key.ts +++ b/packages/cli/src/commands/tokenplan/create-key.ts @@ -1,21 +1,26 @@ 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"; +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_VERSION = "2026-02-10"; const API_ACTION = "CreateTokenPlanKey"; const API_PATH = "/tokenplan/api-keys"; @@ -25,24 +30,10 @@ export default defineCommand({ usage: "bl tokenplan create-key --account-id --workspace-id [flags]", options: [ { flag: "--account-id ", description: "Target member account ID", required: true }, - { - flag: "--workspace-id ", - description: "Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id)", - }, + TOKEN_PLAN_WORKSPACE_OPTION, { flag: "--description ", description: "API key description" }, - { - flag: "--caller-uac-account-id ", - description: "Caller UAC account ID", - }, - { - flag: "--namespace-id ", - description: "Product namespace ID (Token Plan default: namespace-1)", - }, - { flag: "--access-key-id ", description: "Alibaba Cloud Access Key ID (deprecated)" }, - { - flag: "--access-key-secret ", - description: "Alibaba Cloud Access Key Secret (deprecated)", - }, + ...TOKEN_PLAN_COMMON_QUERY_OPTIONS, + ...TOKEN_PLAN_AK_OPTIONS, ], examples: [ "bl tokenplan create-key --account-id acc_123 --workspace-id ws_456", @@ -50,77 +41,35 @@ export default defineCommand({ ], 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 credentials = resolveTokenPlanCredentials(config, flags); const accountId = flags.accountId as string | undefined; - const workspaceId = (flags.workspaceId as string) || config.workspaceId; + const workspaceId = requireWorkspaceId(config, flags); 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 ", - 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); + const { endpoint, queryParams: query } = prepareTokenPlanRequest( + config, + API_PATH, + queryParams, + ); + emitResult({ endpoint, query }, format); return; } - const headers = signRequest({ - accessKeyId, - accessKeySecret, + const data = await callTokenPlanApi({ + config, + credentials, action: API_ACTION, - version: API_VERSION, - body: "", - host, - pathname: API_PATH, + path: API_PATH, method: "POST", - queryString, + queryParams, }); - 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 { @@ -132,14 +81,13 @@ export default defineCommand({ function buildQueryParams( flags: GlobalFlags, resolved: { accountId: string; workspaceId: string }, -): Record { - const params: Record = {}; +): TokenPlanQueryParams { + const params: TokenPlanQueryParams = {}; 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; + appendCommonQueryParams(params, flags); return params; } diff --git a/packages/cli/src/commands/tokenplan/seats.ts b/packages/cli/src/commands/tokenplan/seats.ts index 879ee01..ab4a2c6 100644 --- a/packages/cli/src/commands/tokenplan/seats.ts +++ b/packages/cli/src/commands/tokenplan/seats.ts @@ -1,22 +1,24 @@ import { defineCommand, - buildCanonicalQuery, - signRequest, - modelStudioHost, detectOutputFormat, - maskToken, - trackingHeaders, type Config, type GlobalFlags, - type GetSubscriptionSeatDetailsResponse, - type TokenPlanSeatDetail, BailianError, ExitCode, } from "bailian-cli-core"; import { emitResult, emitBare } from "../../output/output.ts"; import { padEnd } from "../../output/cjk-width.ts"; +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_VERSION = "2026-02-10"; const API_ACTION = "GetSubscriptionSeatDetails"; const API_PATH = "/tokenplan/subscription/seat-detail"; @@ -27,14 +29,7 @@ export default defineCommand({ options: [ { flag: "--page-no ", description: "Page number (default: 1)", type: "number" }, { flag: "--page-size ", description: "Page size (default: 10)", type: "number" }, - { - flag: "--caller-uac-account-id ", - description: "Caller UAC account ID", - }, - { - flag: "--namespace-id ", - description: "Product namespace ID (Token Plan default: namespace-1)", - }, + ...TOKEN_PLAN_COMMON_QUERY_OPTIONS, { flag: "--status ", description: @@ -54,11 +49,7 @@ export default defineCommand({ flag: "--query-assigned ", description: "Filter by assignment: true=assigned, false=unassigned", }, - { flag: "--access-key-id ", description: "Alibaba Cloud Access Key ID (deprecated)" }, - { - flag: "--access-key-secret ", - description: "Alibaba Cloud Access Key Secret (deprecated)", - }, + ...TOKEN_PLAN_AK_OPTIONS, ], examples: [ "bl tokenplan seats", @@ -67,64 +58,28 @@ export default defineCommand({ ], 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 credentials = resolveTokenPlanCredentials(config, flags); 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); + const { endpoint, queryParams: query } = prepareTokenPlanRequest( + config, + API_PATH, + queryParams, + ); + emitResult({ endpoint, query }, format); return; } - const headers = signRequest({ - accessKeyId, - accessKeySecret, + const data = await callTokenPlanApi({ + config, + credentials, action: API_ACTION, - version: API_VERSION, - body: "", - host, - pathname: API_PATH, + path: API_PATH, method: "GET", - queryString, + queryParams, }); - if (config.verbose) { - process.stderr.write(`> GET ${endpoint}\n`); - process.stderr.write(`> AK: ${maskToken(accessKeyId)}\n`); - } - - const timeoutMs = config.timeout * 1000; - const res = await fetch(endpoint, { - method: "GET", - headers: { ...headers, ...trackingHeaders() }, - signal: AbortSignal.timeout(timeoutMs), - }); - - if (config.verbose) { - process.stderr.write(`< ${res.status} ${res.statusText}\n`); - } - - const data = (await res.json()) as GetSubscriptionSeatDetailsResponse; - - if (!res.ok || data.Success === false) { - throw new BailianError( - `${data.Code || res.status} - ${data.Message || res.statusText}`, - ExitCode.GENERAL, - ); - } - const items = data.Data?.Items ?? []; if (config.quiet || format === "text") { emitTextSeats(items, data.Data?.Total, data.Data?.PageNo, data.Data?.PageSize); @@ -134,13 +89,12 @@ export default defineCommand({ }, }); -function buildQueryParams(flags: GlobalFlags): Record { - const params: Record = {}; +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); - if (flags.callerUacAccountId) params.CallerUacAccountId = flags.callerUacAccountId as string; - if (flags.namespaceId) params.NamespaceId = flags.namespaceId as string; + appendCommonQueryParams(params, flags); if (flags.statusListStr) params.StatusListStr = flags.statusListStr as string; const status = flags.status as string[] | undefined; diff --git a/packages/cli/src/commands/tokenplan/types.ts b/packages/cli/src/commands/tokenplan/types.ts new file mode 100644 index 0000000..86fa661 --- /dev/null +++ b/packages/cli/src/commands/tokenplan/types.ts @@ -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; + }; +} diff --git a/packages/cli/src/commands/tokenplan/utils.ts b/packages/cli/src/commands/tokenplan/utils.ts new file mode 100644 index 0000000..674295a --- /dev/null +++ b/packages/cli/src/commands/tokenplan/utils.ts @@ -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 ", description: "Alibaba Cloud Access Key ID (deprecated)" }, + { + flag: "--access-key-secret ", + description: "Alibaba Cloud Access Key Secret (deprecated)", + }, +]; + +export const TOKEN_PLAN_COMMON_QUERY_OPTIONS: OptionDef[] = [ + { + flag: "--caller-uac-account-id ", + description: "Caller UAC account ID", + }, + { + flag: "--namespace-id ", + description: "Product namespace ID (Token Plan default: namespace-1)", + }, +]; + +export const TOKEN_PLAN_WORKSPACE_OPTION: OptionDef = { + flag: "--workspace-id ", + description: "Workspace ID (env: BAILIAN_WORKSPACE_ID, config: workspace_id)", +}; + +const MODEL_STUDIO_HOSTS: Partial> = { + 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; + +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 ", + 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(opts: { + config: Config; + credentials: { accessKeyId: string; accessKeySecret: string }; + action: string; + path: string; + method: "GET" | "POST"; + queryParams: TokenPlanQueryParams; +}): Promise { + 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; +} diff --git a/packages/cli/tests/e2e/helpers.ts b/packages/cli/tests/e2e/helpers.ts index 63d5b97..e35b8a3 100644 --- a/packages/cli/tests/e2e/helpers.ts +++ b/packages/cli/tests/e2e/helpers.ts @@ -136,15 +136,6 @@ export function isKnowledgeAkSkReady(): boolean { ); } -/** Token Plan POP commands (AK/SK only). */ -export function isTokenPlanAkSkReady(): boolean { - return ( - isBailianE2EEnabled() && - !!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID && - !!process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET - ); -} - export interface RunCliResult { stdout: string; stderr: string; diff --git a/packages/cli/tests/e2e/tokenplan.e2e.test.ts b/packages/cli/tests/e2e/tokenplan.e2e.test.ts deleted file mode 100644 index e264cbf..0000000 --- a/packages/cli/tests/e2e/tokenplan.e2e.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { tmpdir } from "os"; -import { describe, expect, test } from "vite-plus/test"; -import { isTokenPlanAkSkReady, parseStdoutJson, runCli } from "./helpers.ts"; - -interface DryRunBody { - endpoint?: string; - query?: Record; -} - -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 () => { - const { stderr, exitCode } = await runCli(["tokenplan", "seats", "--help"]); - expect(exitCode, stderr).toBe(0); - expect(stderr).toMatch(/--page-no/i); - expect(stderr).toMatch(/--page-size/i); - expect(stderr).toMatch(/--seat-id/i); - expect(stderr).toMatch(/--status/i); - expect(stderr).toMatch(/--query-assigned/i); - }); - - 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 errors", () => { - test("seats 无任何凭证时提示 No credentials found 并非零退出", async () => { - const { stderr, exitCode } = await runCli( - ["tokenplan", "seats", "--non-interactive", "--output", "json"], - 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.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", - "seats", - "--dry-run", - "--page-no", - "1", - "--page-size", - "10", - "--status", - "NORMAL", - "--query-assigned", - "true", - "--non-interactive", - "--output", - "json", - ], - fakeAkEnv, - ); - expect(exitCode, stderr).toBe(0); - const data = parseStdoutJson(stdout); - expect(data.endpoint).toMatch(/\/tokenplan\/subscription\/seat-detail/); - expect(data.query?.PageNo).toBe("1"); - expect(data.query?.PageSize).toBe("10"); - expect(data.query?.QueryAssigned).toBe("true"); - expect(data.query?.StatusList).toEqual(["NORMAL"]); - }); - - 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(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(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(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"]); - expect(data.endpoint).toMatch(/AccountIds\.1=acc_1/); - expect(data.endpoint).toMatch(/AccountIds\.2=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(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)", () => { - test("GetSubscriptionSeatDetails 真实调用", async () => { - const { stdout, stderr, exitCode } = await runCli([ - "tokenplan", - "seats", - "--page-size", - "5", - "--non-interactive", - "--output", - "json", - ]); - expect(exitCode, stderr).toBe(0); - const data = parseStdoutJson<{ Success?: boolean; Data?: { Items?: unknown[] } }>(stdout); - expect(data.Success).toBe(true); - expect(Array.isArray(data.Data?.Items)).toBe(true); - }); -}); diff --git a/packages/core/src/client/ak-sign.ts b/packages/core/src/client/ak-sign.ts index 1f55c46..e9ed7be 100644 --- a/packages/core/src/client/ak-sign.ts +++ b/packages/core/src/client/ak-sign.ts @@ -18,33 +18,6 @@ export interface AkSignConfig { host: string; pathname: string; method?: string; - /** ACS3 canonical query string (sorted, encoded, no leading `?`). Empty for POST body-only APIs. */ - queryString?: string; -} - -/** Build ACS3 canonical query string from POP query parameters. */ -export function buildCanonicalQuery(params: Record): string { - 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 signRequest(cfg: AkSignConfig): Record { @@ -74,13 +47,11 @@ export function signRequest(cfg: AkSignConfig): Record { const signedHeadersStr = signedHeaderKeys.join(";"); - const queryString = cfg.queryString ?? ""; - // Build canonical request const canonicalRequest = [ method, cfg.pathname, - queryString, + "", // query string (empty for POST) canonicalHeaders, signedHeadersStr, hashedBody, diff --git a/packages/core/src/client/endpoints.ts b/packages/core/src/client/endpoints.ts index cbcfa20..7cb4ab2 100644 --- a/packages/core/src/client/endpoints.ts +++ b/packages/core/src/client/endpoints.ts @@ -1,16 +1,3 @@ -import type { Region } from "../config/schema.ts"; - -const MODEL_STUDIO_HOSTS: Record = { - cn: "modelstudio.cn-beijing.aliyuncs.com", - us: "modelstudio.cn-beijing.aliyuncs.com", - intl: "modelstudio.ap-southeast-1.aliyuncs.com", -}; - -/** ModelStudio POP OpenAPI host for the given DashScope region preset. */ -export function modelStudioHost(region: Region): string { - return MODEL_STUDIO_HOSTS[region] ?? MODEL_STUDIO_HOSTS.cn; -} - // ---- Chat (OpenAI Compatible) ---- export function chatEndpoint(baseUrl: string): string { diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 5c334c7..22be23e 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -1,5 +1,5 @@ export type { AkSignConfig } from "./ak-sign.ts"; -export { buildCanonicalQuery, signRequest } from "./ak-sign.ts"; +export { signRequest } from "./ak-sign.ts"; export { appCompletionEndpoint, chatEndpoint, @@ -10,7 +10,6 @@ export { memoryListEndpoint, memoryNodeEndpoint, memorySearchEndpoint, - modelStudioHost, mcpWebSearchEndpoint, profileSchemaEndpoint, speechRecognizeEndpoint, diff --git a/packages/core/src/types/api.ts b/packages/core/src/types/api.ts index cdf6bf5..87f0782 100644 --- a/packages/core/src/types/api.ts +++ b/packages/core/src/types/api.ts @@ -417,76 +417,6 @@ export interface DashScopeKnowledgeRetrieveResponse { }; } -// ---- Token Plan / ModelStudio POP (2026-02-10) ---- - -export interface TokenPlanSeatEquity { - EquityType?: string; - CycleInstanceId?: string; - CycleStartTime?: number; - CycleEndTime?: number; - CycleTotalValue?: number; - CycleSurplusValue?: number; - CycleVersion?: number; -} - -export interface TokenPlanSeatDetail { - InstanceCode?: string; - EquityList?: TokenPlanSeatEquity[]; - EndTime?: number; - SeatId?: string; - SpecType?: string; - StartTime?: number; - AssignedStatus?: string; - AccountId?: string; - AccountName?: string; - AccountEmail?: string; - Status?: string; -} - -export interface GetSubscriptionSeatDetailsResponse { - Success?: boolean; - Code?: string; - Message?: string; - Data?: { - Items?: TokenPlanSeatDetail[]; - Total?: number; - PageNo?: number; - PageSize?: number; - }; -} - -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 { diff --git a/packages/core/tests/ak-sign.test.ts b/packages/core/tests/ak-sign.test.ts deleted file mode 100644 index 6633ee8..0000000 --- a/packages/core/tests/ak-sign.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { expect, test } from "vite-plus/test"; -import { buildCanonicalQuery } from "../src/client/ak-sign.ts"; - -test("buildCanonicalQuery flattens arrays as indexed keys", () => { - expect( - buildCanonicalQuery({ - WorkspaceId: "ws_1", - SeatType: "pro", - AccountIds: ["acc_1", "acc_2"], - }), - ).toBe("AccountIds.1=acc_1&AccountIds.2=acc_2&SeatType=pro&WorkspaceId=ws_1"); -}); - -test("buildCanonicalQuery uses single indexed key for one-element arrays", () => { - expect( - buildCanonicalQuery({ - AccountIds: ["acc_2bd88814c31743d9aa5833dc16b3b8e0"], - }), - ).toBe("AccountIds.1=acc_2bd88814c31743d9aa5833dc16b3b8e0"); -});