Skip to content

Commit e1532bf

Browse files
committed
feat(bootstrap): 新增创建控制台用户步骤,完善服务激活流程
- 在 bootstrap 命令流程中添加第 3 步,调用 createUser 接口创建控制台账号用户 - 修改 callApi 函数支持传入请求体参数 - 增强错误日志输出,显示更完整的错误响应内容 - 在服务启动时校验登录信息包含 aliyun.uid,否则抛出错误 - 优化商品检查与激活逻辑,更清晰的状态输出及错误处理 - 在 CLI-core 控制台网关请求中添加日志,打印结构化请求负载,支持 verbose 模式 - BailianError 新增 rawResponse 字段,保存原始错误响应数据 - 增加 bootstrap 命令单元测试,覆盖用户创建及不同初始化场景 - 调整测试框架用例,增加调用控制台网关及错误处理的测试覆盖
1 parent 3fb0c72 commit e1532bf

6 files changed

Lines changed: 331 additions & 19 deletions

File tree

cli-access-token.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## 登录
2+
3+
https://signin.aliyun.com/1062516667359476.onaliyun.com/login.htm
4+
lisheng@1062516667359476.onaliyun.com
5+
app$$5%%%Ehiliao
6+
7+
## 获得 AK SK STS 三元组
8+
9+
```
10+
pnpm bl auth generate-access-token --access-key-id STS.NXsfUgEqhDQBJTz1V5JJSMRDw --access-key-secret CmJU9so7yMTjZ3mpVF9eMqFSZXkEn2LhFMogi1hfn8rk --security-token CAIS1gJ1q6Ft5B2yfSjIr5vGLe/TqK5J85OpSHLL1VZgRsV/opfvlTz2IHhMe3BtAuwXtvQ1mG9R7/0ZlqBpR4RIXlfFas0oFyyqTp/6MeT7oMWQweEuqv/MQBq+aXPS2MvVfJ+KLrf0ceusbFbpjzJ6xaCAGxypQ12iN+/i6/clFKN1ODO1dj1bHtxbCxJ/ocsBTxvrOO2qLwThjxi7biMqmHIl2T8ns/vlnpbHs0KP0gWq8IJP+dSteKrDRtJ3IZJyX+2y2OFLbafb2EdSkUMSrPgv0fcYqG+X5I3CWgAKuA/MKefP9cB1JwJ1Z7I3ELJDtun1nvZ/p+rPno/8xg1WJ+ZRXjRD7XJMD2hdcQnAF6HaFd6TUxylurgExgnkPL5jz1gvlRKYWhvQG45hiCZWPhXwAIHJtv6kTMnd5abLPm9I37QLATeM356+Q3LrJRHx74QEOMJUBysagAFIsU+wgReSHvEEXx5y3qyr3JB3t7tY9nI0FrIx0GudXEjl2vE4sD7aTxleD1Weqlq8XAzIu8DrdU8tkNdQ0rGricEZd9DY2WWDer1eD7IdoLuavwGHXyLaJkw1c1UPq2O3fKp2EQ8b5zei/Ep+MGfMCYDHyVqDPAPLD5yTtN6eOCAA
11+
```

packages/commands/src/commands/bootstrap/index.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { emitResult, emitBare } from "bailian-cli-runtime";
44
const API = {
55
loginInfo: "zeldaEasy.cornerstone-portal.cs-console.loginInfo",
66
initSpace: "zeldaEasy.bailian-dash-workspace.space.initSpace",
7+
createUser: "zeldaEasy.bailian-dash-workspace.account.createUser",
78
queryBuyResult: "zeldaEasy.bailian-commerce.bill.queryBuyPostpaidResult",
89
commodityOrderInfo: "zeldaEasy.bailian-commerce.bill.postpaidCommodityOrderInfo",
910
buyCommodity: "zeldaEasy.bailian-commerce.bill.buyPostpaidCommodity",
@@ -51,16 +52,21 @@ export default defineCommand({
5152
},
5253
{
5354
step: 3,
55+
api: API.createUser,
56+
description: "Create console account user",
57+
},
58+
{
59+
step: 4,
5460
api: API.queryBuyResult,
5561
description: "Query postpaid order status",
5662
},
5763
{
58-
step: 4,
64+
step: 5,
5965
api: API.commodityOrderInfo,
6066
description: "Query commodity activation status",
6167
},
6268
{
63-
step: 5,
69+
step: 6,
6470
api: API.buyCommodity,
6571
description: "Activate postpaid commodities (if needed)",
6672
},
@@ -72,14 +78,18 @@ export default defineCommand({
7278
}
7379

7480
const verbose = settings.verbose;
75-
const callApi = async (api: string) => {
81+
const callApi = async (api: string, data: Record<string, unknown> = {}) => {
7682
if (verbose) process.stderr.write(`> ${api}\n`);
7783
try {
78-
const resp = await ctx.client.console<any>(api, {});
84+
const resp = await ctx.client.console<any>(api, data);
7985
if (verbose) process.stderr.write(`< ${JSON.stringify(resp)}\n`);
8086
return resp;
8187
} catch (err) {
82-
if (verbose) process.stderr.write(`< ERROR: ${err instanceof Error ? err.message : err}\n`);
88+
if (verbose) {
89+
const message =
90+
err instanceof BailianError ? (err.rawResponse ?? err.message) : String(err);
91+
process.stderr.write(`< ERROR: ${message}\n`);
92+
}
8393
throw err;
8494
}
8595
};
@@ -99,12 +109,25 @@ export default defineCommand({
99109
emitBare("Workspace already initialized.");
100110
}
101111

102-
// Step 3-5: Order & commodity flow
112+
// Step 3: Create console user
113+
const uid = loginData?.aliyun?.uid;
114+
if (typeof uid !== "string" || uid.length === 0) {
115+
throw new BailianError("Console login info did not include aliyun.uid.", ExitCode.GENERAL);
116+
}
117+
await callApi(API.createUser, {
118+
reqDTO: {
119+
outerKey: uid,
120+
nickName: uid,
121+
userName: uid,
122+
},
123+
});
124+
125+
// Step 4-6: Order & commodity flow
103126
await ensureCommoditiesActive(callApi, format);
104127
},
105128
});
106129

107-
type ApiCall = (api: string) => Promise<any>;
130+
type ApiCall = (api: string, data?: Record<string, unknown>) => Promise<any>;
108131

109132
async function ensureCommoditiesActive(call: ApiCall, format: "text" | "json"): Promise<void> {
110133
emitBare("Checking service activation status...");
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { expect, test } from "vite-plus/test";
2+
import bootstrapCommand from "../src/commands/bootstrap/index.ts";
3+
4+
const API = {
5+
loginInfo: "zeldaEasy.cornerstone-portal.cs-console.loginInfo",
6+
initSpace: "zeldaEasy.bailian-dash-workspace.space.initSpace",
7+
createUser: "zeldaEasy.bailian-dash-workspace.account.createUser",
8+
queryBuyResult: "zeldaEasy.bailian-commerce.bill.queryBuyPostpaidResult",
9+
commodityOrderInfo: "zeldaEasy.bailian-commerce.bill.postpaidCommodityOrderInfo",
10+
buyCommodity: "zeldaEasy.bailian-commerce.bill.buyPostpaidCommodity",
11+
} as const;
12+
13+
interface ConsoleCall {
14+
api: string;
15+
data: Record<string, unknown>;
16+
}
17+
18+
function captureStdout(): { read: () => string; restore: () => void } {
19+
const originalWrite = process.stdout.write.bind(process.stdout);
20+
let stdout = "";
21+
process.stdout.write = ((chunk: string | Uint8Array) => {
22+
stdout += String(chunk);
23+
return true;
24+
}) as typeof process.stdout.write;
25+
return {
26+
read: () => stdout,
27+
restore: () => {
28+
process.stdout.write = originalWrite;
29+
},
30+
};
31+
}
32+
33+
function gatewayResponse(data: unknown): unknown {
34+
return {
35+
data: {
36+
DataV2: {
37+
data: {
38+
data,
39+
},
40+
},
41+
success: true,
42+
},
43+
};
44+
}
45+
46+
function createContext(
47+
consoleImpl: (api: string, data: Record<string, unknown>) => Promise<unknown>,
48+
) {
49+
return {
50+
settings: {
51+
dryRun: false,
52+
output: "json",
53+
verbose: false,
54+
},
55+
client: {
56+
console: consoleImpl,
57+
},
58+
};
59+
}
60+
61+
test("bootstrap --dry-run lists createUser after initSpace", async () => {
62+
const stdout = captureStdout();
63+
try {
64+
await bootstrapCommand.run({
65+
...createContext(async () => ({})),
66+
settings: { dryRun: true, output: "json", verbose: false },
67+
} as any);
68+
} finally {
69+
stdout.restore();
70+
}
71+
72+
const data = JSON.parse(stdout.read()) as {
73+
apis: Array<{ step: number; api: string }>;
74+
};
75+
expect(data.apis.map((item) => item.api)).toEqual([
76+
API.loginInfo,
77+
API.initSpace,
78+
API.createUser,
79+
API.queryBuyResult,
80+
API.commodityOrderInfo,
81+
API.buyCommodity,
82+
]);
83+
expect(data.apis.map((item) => item.step)).toEqual([1, 2, 3, 4, 5, 6]);
84+
});
85+
86+
test("bootstrap creates user from loginInfo uid when workspace is not initialized", async () => {
87+
const uid = "AssumedRoleUser300715349082471133";
88+
const calls: ConsoleCall[] = [];
89+
const stdout = captureStdout();
90+
const ctx = createContext(async (api, data) => {
91+
calls.push({ api, data });
92+
if (api === API.loginInfo) {
93+
return gatewayResponse({ spaceInited: false, aliyun: { uid } });
94+
}
95+
if (api === API.queryBuyResult) {
96+
return gatewayResponse("success");
97+
}
98+
if (api === API.commodityOrderInfo) {
99+
return gatewayResponse([{ commodityCode: "postpaid", status: 10 }]);
100+
}
101+
return gatewayResponse({});
102+
});
103+
104+
try {
105+
await bootstrapCommand.run(ctx as any);
106+
} finally {
107+
stdout.restore();
108+
}
109+
110+
expect(calls.map((call) => call.api)).toEqual([
111+
API.loginInfo,
112+
API.initSpace,
113+
API.createUser,
114+
API.queryBuyResult,
115+
API.commodityOrderInfo,
116+
]);
117+
expect(calls.find((call) => call.api === API.createUser)?.data).toEqual({
118+
reqDTO: {
119+
outerKey: uid,
120+
nickName: uid,
121+
userName: uid,
122+
},
123+
});
124+
});
125+
126+
test("bootstrap skips initSpace but still creates user when workspace is already initialized", async () => {
127+
const uid = "AssumedRoleUser300715349082471133";
128+
const calls: ConsoleCall[] = [];
129+
const stdout = captureStdout();
130+
const ctx = createContext(async (api, data) => {
131+
calls.push({ api, data });
132+
if (api === API.loginInfo) {
133+
return gatewayResponse({
134+
spaceInited: true,
135+
aliyun: { uid },
136+
});
137+
}
138+
if (api === API.queryBuyResult) {
139+
return gatewayResponse("success");
140+
}
141+
if (api === API.commodityOrderInfo) {
142+
return gatewayResponse([{ commodityCode: "postpaid", status: 10 }]);
143+
}
144+
return gatewayResponse({});
145+
});
146+
147+
try {
148+
await bootstrapCommand.run(ctx as any);
149+
} finally {
150+
stdout.restore();
151+
}
152+
153+
expect(calls.map((call) => call.api)).toEqual([
154+
API.loginInfo,
155+
API.createUser,
156+
API.queryBuyResult,
157+
API.commodityOrderInfo,
158+
]);
159+
expect(calls.some((call) => call.api === API.initSpace)).toBe(false);
160+
expect(calls.find((call) => call.api === API.createUser)?.data).toEqual({
161+
reqDTO: {
162+
outerKey: uid,
163+
nickName: uid,
164+
userName: uid,
165+
},
166+
});
167+
});

packages/core/src/console/gateway.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ interface ConsoleGatewayInfo {
1313

1414
const REGION_GATEWAYS: Record<string, Record<ConsoleSite, ConsoleGatewayInfo>> = {
1515
"cn-beijing": {
16-
domestic: { csGateway: "bailian-cs.console.aliyun.com", action: "BroadScopeAspnGateway" },
16+
domestic: {
17+
csGateway: "bailian-cs.console.aliyun.com",
18+
action: "BroadScopeAspnGateway",
19+
},
1720
international: {
1821
csGateway: "bailian-cs.console.alibabacloud.com",
1922
action: "BroadScopeAspnGateway",
@@ -74,6 +77,7 @@ function buildGatewayParams(
7477
protocol: "V2",
7578
console: "ONE_CONSOLE",
7679
productCode: "p_efm",
80+
switchUserType: 3,
7781
consoleSite: "BAILIAN_ALIYUN",
7882
...(switchAgent != null ? { switchAgent } : {}),
7983
...(typeof data.cornerstoneParam === "object" && data.cornerstoneParam !== null
@@ -119,6 +123,9 @@ export async function callConsoleGateway(
119123
const endpoint = `${gatewayBase}/cli/api.json?action=${action}&product=${GATEWAY_PRODUCT}&api=${encodeURIComponent(api)}`;
120124
if (settings?.verbose) {
121125
process.stderr.write(`> POST ${endpoint}\n`);
126+
process.stderr.write(
127+
`> payload ${JSON.stringify({ params: JSON.parse(params), region: target.region }, null, 2)}\n`,
128+
);
122129
}
123130

124131
const res = await fetch(endpoint, {
@@ -142,27 +149,23 @@ export async function callConsoleGateway(
142149
}
143150

144151
const json = (await res.json()) as Record<string, unknown>;
145-
if (settings?.verbose) {
146-
process.stderr.write(`< ${JSON.stringify(json)}\n`);
147-
}
148152

149153
const innerData = json.data as Record<string, unknown> | undefined;
150154
if (innerData?.success === false && innerData.errorCode) {
155+
const rawResponse = JSON.stringify(json);
151156
const rawErrorCode = innerData.errorCode;
152157
const errorCode =
153158
typeof rawErrorCode === "string" ? rawErrorCode : JSON.stringify(rawErrorCode);
154159
const notLogined = errorCode.includes("NotLogined");
155-
const errorMsg = typeof innerData.errorMsg === "string" ? innerData.errorMsg : undefined;
156160
throw new BailianError(
157161
notLogined
158162
? "Console session is not logged in or has expired."
159163
: `Console gateway error: ${errorCode}`,
160164
notLogined ? ExitCode.AUTH : ExitCode.GENERAL,
161165
notLogined
162166
? "Run `bl auth login --console` to sign in or refresh your console session."
163-
: errorMsg && errorMsg !== errorCode
164-
? errorMsg
165-
: undefined,
167+
: undefined,
168+
{ rawResponse },
166169
);
167170
}
168171

packages/core/src/errors/base.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ export interface ApiErrorContext {
99
export interface BailianErrorOptions {
1010
cause?: unknown;
1111
api?: ApiErrorContext;
12+
rawResponse?: string;
1213
}
1314

1415
export class BailianError extends Error {
1516
readonly exitCode: ExitCode;
1617
readonly hint?: string;
1718
readonly api?: ApiErrorContext;
19+
readonly rawResponse?: string;
1820

1921
constructor(
2022
message: string,
@@ -27,6 +29,7 @@ export class BailianError extends Error {
2729
this.exitCode = exitCode;
2830
this.hint = hint;
2931
this.api = options?.api;
32+
this.rawResponse = options?.rawResponse;
3033
}
3134

3235
toJSON() {

0 commit comments

Comments
 (0)