@restoai/resto-datacli 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,19 +6,56 @@ import { AuthStore } from "../config/auth-store.js";
6
6
  import { failJson, printJson } from "./json-command.js";
7
7
  const DEFAULT_CHINA_LANGUAGE_CODE = "zh_CN";
8
8
  const DEFAULT_CHINA_TIMEZONE = "Asia/Shanghai";
9
- const DEFAULT_CHINA_BASE_URL = "https://bo.restosuite.cn";
10
- const PASSWORD_LOGIN_PATH = "/loginApi/loginForToken";
9
+ const DEFAULT_AUTH_GATEWAY_BASE_URL = "https://cli.test.restosuite.ai";
10
+ const DEFAULT_AUTH_GATEWAY_PLAT = "cli";
11
+ const LOGIN_FOR_TOKEN_PATH = "/loginApi/loginForToken";
12
+ const PHONE_CAPTCHA_PATH = "/vulcan/captcha/phoneCaptcha";
11
13
  const PROFILE_INFO_PATH = "/vulcan/profile/getInfo";
12
14
  const REPORT_SHOP_ORG_TYPE_LIST = [7];
15
+ const PHONE_CAPTCHA_TYPE_VERIFY_CODE = "1";
16
+ const PHONE_CAPTCHA_STEP_LOGIN = "1";
17
+ const DEFAULT_PHONE_AREA_CODE = "86";
18
+ const DEFAULT_PHONE_REGION = "CN";
19
+ const STEP_CHECK_MOBILE = "StepCheckMobile";
13
20
  const STEP_CHECK_EMAIL = "StepCheckEmail";
14
21
  const STEP_EMAIL = "StepEmail";
22
+ const STEP_MOBILE = "StepMobile";
15
23
  const STEP_SELECT_TENANT = "StepSelectTenant";
16
- const BO_PASSWORD_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
24
+ const AUTH_GATEWAY_PASSWORD_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
17
25
  MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAo+oABY2n4ff6+ahItStjvnYIPYswCwqhIHMw8IZyWVBv3ygiIbv6Cj2rteiNTQtPZZN/Q9dyASSE7FSaYsc6U/TDYLV40XsywGp4lIzuoq4CTf3Z71exdagUiy5ZAB4Wp+0RUpv8BAA97uLskt/LGLz/5SP66dmZBHkw8NkYppn4VTsLo9JWOGg8PXr3tYs9BDnYh7PiqVcNaS1POM3DQoIMda4mmLez/5nL8SW6KAdKniPEjZFVj3w1ZTeiT4TjCpZaVfVQDpLy0Vs0mKfIf63QXI+wjuxuvEcBcKzM9UAfX+I6gVnSwjRNu2U7HJmux/1vitmT4i/OPS7ZNMTnoQLJm1oWDivr3/JLnyMJKoAmIWUXgWnkb7S0lGA3xF4OdcizRlZyZFLhczNaq6D/ZTcQxu1DfU2/kmNh5JTZgC+50rTb4wxiaIci8cxT4hZsdkUfhE41k/Q7TNGVJZnuQHKRMPC2W10gQcJWG2/gLBv55OB98DE3aE7HVKa3QMVQybm/YjMtt8V+7BhsGiQBTjtVYNdiAHn9brfkhRESxfQ+boIl7n7V8DyMzMnhhZ6K65+whvg8mho5sJCOuI+u+9SCy0WhgpEcZxVG1hxC3oiLkGix0zsH/xEvdCd9xo2HNUXzrBy0Yt4C0VbpKQaD57ZM5vlVNP1ZwwelJi9K5XsCAwEAAQ==
18
26
  -----END PUBLIC KEY-----`;
19
27
  function trimTrailingSlash(value) {
20
28
  return value.replace(/\/+$/, "");
21
29
  }
30
+ function normalizeOptionalString(value) {
31
+ const trimmed = value?.trim();
32
+ return trimmed ? trimmed : undefined;
33
+ }
34
+ function appendQueryParam(url, key, value) {
35
+ const normalizedValue = normalizeOptionalString(value);
36
+ if (!normalizedValue) {
37
+ return url;
38
+ }
39
+ const hashIndex = url.indexOf("#");
40
+ const base = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
41
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
42
+ if (new RegExp(`(^|[?&])${key}=`).test(base)) {
43
+ return url;
44
+ }
45
+ const separator = base.includes("?") ? "&" : "?";
46
+ return `${base}${separator}${encodeURIComponent(key)}=${encodeURIComponent(normalizedValue)}${hash}`;
47
+ }
48
+ function authGatewayUrl(baseUrl, path, plat) {
49
+ return appendQueryParam(`${trimTrailingSlash(baseUrl)}${path}`, "plat", plat);
50
+ }
51
+ function profileFromLoginOptions(options, profile) {
52
+ const plat = normalizeOptionalString(options.plat);
53
+ return {
54
+ baseUrl: options.baseUrl,
55
+ ...(plat ? { plat } : {}),
56
+ ...profile
57
+ };
58
+ }
22
59
  function asRecord(value) {
23
60
  return value && typeof value === "object" ? value : undefined;
24
61
  }
@@ -54,9 +91,9 @@ function extractLoginError(payload) {
54
91
  const root = asRecord(payload);
55
92
  return firstString(root?.msg, root?.message, root?.errorMessage);
56
93
  }
57
- function encryptBoPassword(password) {
94
+ function encryptGatewayPassword(password) {
58
95
  return publicEncrypt({
59
- key: BO_PASSWORD_PUBLIC_KEY,
96
+ key: AUTH_GATEWAY_PASSWORD_PUBLIC_KEY,
60
97
  padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING,
61
98
  oaepHash: "sha256"
62
99
  }, Buffer.from(password)).toString("base64");
@@ -72,22 +109,6 @@ function parseIntegerList(value) {
72
109
  const numbers = parts.map((part) => Number.parseInt(part, 10));
73
110
  return numbers.every((number) => Number.isInteger(number) && number > 0) ? numbers : undefined;
74
111
  }
75
- function jwtPayload(token) {
76
- const parts = token.split(".");
77
- if (parts.length < 2) {
78
- return undefined;
79
- }
80
- try {
81
- return asRecord(JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8")));
82
- }
83
- catch {
84
- return undefined;
85
- }
86
- }
87
- function inferEmployeeCodeFromToken(token) {
88
- const payload = jwtPayload(token);
89
- return firstString(payload?.employeeCode, payload?.loginId, payload?.principalUid);
90
- }
91
112
  function hasCompleteReportContext(reportContext) {
92
113
  return Boolean(reportContext?.employeeCode &&
93
114
  reportContext.organizationId &&
@@ -97,7 +118,7 @@ function hasCompleteReportContext(reportContext) {
97
118
  }
98
119
  function buildReportContext(profile, options) {
99
120
  const orgTypeList = options.orgTypeList ? parseIntegerList(options.orgTypeList) : profile.reportContext?.orgTypeList;
100
- const employeeCode = options.employeeCode ?? profile.reportContext?.employeeCode ?? inferEmployeeCodeFromToken(profile.token);
121
+ const employeeCode = options.employeeCode ?? profile.reportContext?.employeeCode;
101
122
  const organizationId = options.organizationId ?? profile.reportContext?.organizationId;
102
123
  const organizationType = options.organizationType ?? profile.reportContext?.organizationType;
103
124
  const reportContext = {
@@ -121,7 +142,7 @@ function extractProfileReportContext(payload) {
121
142
  };
122
143
  return Object.keys(reportContext).length > 0 ? reportContext : undefined;
123
144
  }
124
- function extractPasswordLoginProfile(payload, fallbackCorporationId) {
145
+ function extractLoginProfile(payload, fallbackCorporationId) {
125
146
  const records = collectRecords(payload);
126
147
  const token = records.reduce((current, record) => current ?? firstString(record.token, record.accessToken, record.access_token), undefined);
127
148
  const employeeCode = records.reduce((current, record) => current ?? firstString(record.employeeCode, record.loginId, record.principalUid), undefined);
@@ -134,7 +155,7 @@ function withReportContext(profile, options) {
134
155
  return reportContext ? { ...profile, reportContext } : profile;
135
156
  }
136
157
  async function fetchProfileReportContext(profile, fetchImpl) {
137
- const response = await fetchImpl(`${trimTrailingSlash(profile.baseUrl)}${PROFILE_INFO_PATH}`, {
158
+ const response = await fetchImpl(authGatewayUrl(profile.baseUrl, PROFILE_INFO_PATH, profile.plat), {
138
159
  method: "GET",
139
160
  headers: {
140
161
  Accept: "application/json",
@@ -172,16 +193,38 @@ async function hydrateReportContext(profile, options, fetchImpl) {
172
193
  }
173
194
  async function loginWithPassword(options, fetchImpl) {
174
195
  if (!options.baseUrl) {
175
- throw new Error("Password login requires --base-url.");
196
+ throw new Error("Username/password login requires --base-url.");
176
197
  }
177
198
  const loginOptions = { ...options, baseUrl: options.baseUrl };
178
199
  if (options.loginFlowCode || options.loginStepName) {
179
200
  return await loginWithExplicitPasswordFlow(loginOptions, fetchImpl);
180
201
  }
181
- return await loginWithBoPasswordSteps(loginOptions, fetchImpl);
202
+ return await loginWithGatewayPasswordSteps(loginOptions, fetchImpl);
182
203
  }
183
204
  async function postLoginForToken(options, fetchImpl, body) {
184
- const response = await fetchImpl(`${trimTrailingSlash(options.baseUrl)}${PASSWORD_LOGIN_PATH}`, {
205
+ const response = await fetchImpl(authGatewayUrl(options.baseUrl, LOGIN_FOR_TOKEN_PATH, options.plat), {
206
+ method: "POST",
207
+ headers: {
208
+ Accept: "application/json",
209
+ "Content-Type": "application/json",
210
+ "Language-Code": options.languageCode,
211
+ "Accept-Timezone": options.timezone
212
+ },
213
+ body: JSON.stringify(body)
214
+ });
215
+ const payload = await response.json();
216
+ if (!response.ok) {
217
+ throw new Error(`Login request failed: ${response.status} ${response.statusText}`.trim());
218
+ }
219
+ const root = asRecord(payload);
220
+ const code = firstString(root?.code);
221
+ if (code && code !== "000") {
222
+ throw new Error(extractLoginError(payload) ?? `Login failed with code ${code}.`);
223
+ }
224
+ return payload;
225
+ }
226
+ async function postPhoneCaptcha(options, fetchImpl, body) {
227
+ const response = await fetchImpl(authGatewayUrl(options.baseUrl, PHONE_CAPTCHA_PATH, options.plat), {
185
228
  method: "POST",
186
229
  headers: {
187
230
  Accept: "application/json",
@@ -193,15 +236,80 @@ async function postLoginForToken(options, fetchImpl, body) {
193
236
  });
194
237
  const payload = await response.json();
195
238
  if (!response.ok) {
196
- throw new Error(`Password login request failed: ${response.status} ${response.statusText}`.trim());
239
+ throw new Error(`Phone dynamic code request failed: ${response.status} ${response.statusText}`.trim());
197
240
  }
198
241
  const root = asRecord(payload);
199
242
  const code = firstString(root?.code);
200
243
  if (code && code !== "000") {
201
- throw new Error(extractLoginError(payload) ?? `Password login failed with code ${code}.`);
244
+ throw new Error(extractLoginError(payload) ?? `Phone dynamic code request failed with code ${code}.`);
202
245
  }
203
246
  return payload;
204
247
  }
248
+ function normalizePhoneAreaCode(value) {
249
+ return normalizeOptionalString(value) ?? DEFAULT_PHONE_AREA_CODE;
250
+ }
251
+ function normalizePhoneRegion(value) {
252
+ return normalizeOptionalString(value) ?? DEFAULT_PHONE_REGION;
253
+ }
254
+ function maskPhoneNumber(phoneNumber) {
255
+ const trimmed = phoneNumber.trim();
256
+ return trimmed.length > 7 ? `${trimmed.slice(0, 3)}****${trimmed.slice(-4)}` : trimmed;
257
+ }
258
+ function formatPhoneCodeSendError(error) {
259
+ const message = error instanceof Error ? error.message : "验证码发送失败";
260
+ return message.replace(/^Phone dynamic code request failed:\s*/, "").trim() || "验证码发送失败";
261
+ }
262
+ function buildPhoneIdentity(options) {
263
+ const phoneNumber = normalizeOptionalString(options.phoneNumber);
264
+ if (!phoneNumber) {
265
+ throw new Error("Phone login requires --phone-number.");
266
+ }
267
+ return {
268
+ phoneAreaCode: normalizePhoneAreaCode(options.phoneAreaCode),
269
+ phoneNumber,
270
+ phoneRegion: normalizePhoneRegion(options.phoneRegion)
271
+ };
272
+ }
273
+ async function sendPhoneDynamicCode(options, fetchImpl) {
274
+ if (!options.corporationId) {
275
+ throw new Error("Phone login requires --corporation-id before sending a dynamic code.");
276
+ }
277
+ const phone = buildPhoneIdentity(options);
278
+ await postPhoneCaptcha(options, fetchImpl, {
279
+ corporationId: options.corporationId,
280
+ captchaType: PHONE_CAPTCHA_TYPE_VERIFY_CODE,
281
+ step: PHONE_CAPTCHA_STEP_LOGIN,
282
+ phone: phone.phoneNumber,
283
+ phoneAreaCode: phone.phoneAreaCode,
284
+ region: phone.phoneRegion
285
+ });
286
+ }
287
+ async function collectInteractivePhoneDynamicCode(prompt, phoneOptions, fetchImpl) {
288
+ let action = await prompt.select(`验证码发送到 ${maskPhoneNumber(phoneOptions.phoneNumber ?? "")}`, [
289
+ { value: "send", label: "发送验证码" },
290
+ { value: "manual", label: "我已获取验证码,继续输入" },
291
+ { value: "cancel", label: "取消登录" }
292
+ ]);
293
+ while (action === "send" || action === "retry") {
294
+ try {
295
+ await sendPhoneDynamicCode(phoneOptions, fetchImpl);
296
+ prompt.message?.("验证码已发送,请输入短信验证码。");
297
+ break;
298
+ }
299
+ catch (error) {
300
+ prompt.message?.(`发送验证码失败:${formatPhoneCodeSendError(error)}`);
301
+ action = await prompt.select("验证码发送失败", [
302
+ { value: "retry", label: "重试发送" },
303
+ { value: "manual", label: "我已获取验证码,继续输入" },
304
+ { value: "cancel", label: "取消登录" }
305
+ ]);
306
+ }
307
+ }
308
+ if (action === "cancel") {
309
+ throw new Error("Interactive login canceled.");
310
+ }
311
+ return await prompt.secret("验证码");
312
+ }
205
313
  function extractFirstNextStep(payload) {
206
314
  const data = asRecord(asRecord(payload)?.data);
207
315
  const nextSteps = data?.nextSteps;
@@ -220,7 +328,7 @@ function extractNextStepTicket(payload) {
220
328
  }
221
329
  function hasCompletedLogin(payload) {
222
330
  const data = asRecord(asRecord(payload)?.data);
223
- return data?.hasNextStep === false || Boolean(extractPasswordLoginProfile(payload).token);
331
+ return data?.hasNextStep === false || Boolean(extractLoginProfile(payload).token);
224
332
  }
225
333
  function inferSingleCorporationId(payload) {
226
334
  const corporationIds = new Set();
@@ -232,7 +340,7 @@ function inferSingleCorporationId(payload) {
232
340
  }
233
341
  return corporationIds.size === 1 ? [...corporationIds][0] : undefined;
234
342
  }
235
- async function loginWithBoPasswordSteps(options, fetchImpl) {
343
+ async function loginWithGatewayPasswordSteps(options, fetchImpl) {
236
344
  const email = options.username ?? "";
237
345
  const password = options.password ?? "";
238
346
  const checkEmailPayload = await postLoginForToken(options, fetchImpl, {
@@ -241,17 +349,16 @@ async function loginWithBoPasswordSteps(options, fetchImpl) {
241
349
  stepName: STEP_CHECK_EMAIL
242
350
  });
243
351
  if (hasCompletedLogin(checkEmailPayload)) {
244
- const profile = extractPasswordLoginProfile(checkEmailPayload, options.corporationId);
352
+ const profile = extractLoginProfile(checkEmailPayload, options.corporationId);
245
353
  if (!profile.token || !profile.corporationId) {
246
354
  throw new Error("Password login response did not contain a token or corporation id.");
247
355
  }
248
- return withReportContext({
249
- baseUrl: options.baseUrl,
356
+ return withReportContext(profileFromLoginOptions(options, {
250
357
  token: profile.token,
251
358
  corporationId: profile.corporationId,
252
359
  languageCode: options.languageCode,
253
360
  timezone: options.timezone
254
- }, { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
361
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
255
362
  }
256
363
  const emailStep = extractFirstNextStep(checkEmailPayload);
257
364
  const emailTicket = extractNextStepTicket(checkEmailPayload);
@@ -260,23 +367,22 @@ async function loginWithBoPasswordSteps(options, fetchImpl) {
260
367
  }
261
368
  const passwordPayload = await postLoginForToken(options, fetchImpl, {
262
369
  email,
263
- password: encryptBoPassword(password),
370
+ password: encryptGatewayPassword(password),
264
371
  ticket: emailTicket,
265
372
  flowCode: emailStep.flowCode,
266
373
  stepName: emailStep.stepName
267
374
  });
268
375
  if (hasCompletedLogin(passwordPayload)) {
269
- const profile = extractPasswordLoginProfile(passwordPayload, options.corporationId);
376
+ const profile = extractLoginProfile(passwordPayload, options.corporationId);
270
377
  if (!profile.token || !profile.corporationId) {
271
378
  throw new Error("Password login response did not contain a token or corporation id.");
272
379
  }
273
- return withReportContext({
274
- baseUrl: options.baseUrl,
380
+ return withReportContext(profileFromLoginOptions(options, {
275
381
  token: profile.token,
276
382
  corporationId: profile.corporationId,
277
383
  languageCode: options.languageCode,
278
384
  timezone: options.timezone
279
- }, { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
385
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
280
386
  }
281
387
  const tenantStep = extractFirstNextStep(passwordPayload);
282
388
  if (!tenantStep || tenantStep.stepName !== STEP_SELECT_TENANT) {
@@ -296,17 +402,90 @@ async function loginWithBoPasswordSteps(options, fetchImpl) {
296
402
  flowCode: tenantStep.flowCode,
297
403
  stepName: tenantStep.stepName
298
404
  });
299
- const profile = extractPasswordLoginProfile(tenantPayload, tenantId);
405
+ const profile = extractLoginProfile(tenantPayload, tenantId);
300
406
  if (!profile.token || !profile.corporationId) {
301
407
  throw new Error("Password login response did not contain a token or corporation id.");
302
408
  }
303
- return withReportContext({
304
- baseUrl: options.baseUrl,
409
+ return withReportContext(profileFromLoginOptions(options, {
410
+ token: profile.token,
411
+ corporationId: profile.corporationId,
412
+ languageCode: options.languageCode,
413
+ timezone: options.timezone
414
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
415
+ }
416
+ async function loginWithPhone(options, fetchImpl) {
417
+ if (!options.baseUrl) {
418
+ throw new Error("Phone login requires --base-url.");
419
+ }
420
+ if (!options.dynamicCode) {
421
+ throw new Error("Phone login requires --dynamic-code.");
422
+ }
423
+ const loginOptions = { ...options, baseUrl: options.baseUrl };
424
+ const phone = buildPhoneIdentity(options);
425
+ const checkMobilePayload = await postLoginForToken(loginOptions, fetchImpl, {
426
+ ...phone,
427
+ flowCode: STEP_CHECK_MOBILE,
428
+ stepName: STEP_CHECK_MOBILE
429
+ });
430
+ if (hasCompletedLogin(checkMobilePayload)) {
431
+ const profile = extractLoginProfile(checkMobilePayload, options.corporationId);
432
+ if (!profile.token || !profile.corporationId) {
433
+ throw new Error("Phone login response did not contain a token or corporation id.");
434
+ }
435
+ return withReportContext(profileFromLoginOptions(loginOptions, {
436
+ token: profile.token,
437
+ corporationId: profile.corporationId,
438
+ languageCode: options.languageCode,
439
+ timezone: options.timezone
440
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
441
+ }
442
+ const mobileStep = extractFirstNextStep(checkMobilePayload);
443
+ if (!mobileStep || mobileStep.stepName !== STEP_MOBILE) {
444
+ throw new Error("Phone login did not return the expected StepMobile step.");
445
+ }
446
+ const mobilePayload = await postLoginForToken(loginOptions, fetchImpl, {
447
+ ...phone,
448
+ dynamicCode: options.dynamicCode,
449
+ flowCode: mobileStep.flowCode,
450
+ stepName: mobileStep.stepName
451
+ });
452
+ if (hasCompletedLogin(mobilePayload)) {
453
+ const profile = extractLoginProfile(mobilePayload, options.corporationId);
454
+ if (!profile.token || !profile.corporationId) {
455
+ throw new Error("Phone login response did not contain a token or corporation id.");
456
+ }
457
+ return withReportContext(profileFromLoginOptions(loginOptions, {
458
+ token: profile.token,
459
+ corporationId: profile.corporationId,
460
+ languageCode: options.languageCode,
461
+ timezone: options.timezone
462
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
463
+ }
464
+ const tenantStep = extractFirstNextStep(mobilePayload);
465
+ if (!tenantStep || tenantStep.stepName !== STEP_SELECT_TENANT) {
466
+ throw new Error("Phone login returned an unsupported next step.");
467
+ }
468
+ const tenantId = options.corporationId ?? inferSingleCorporationId(mobilePayload);
469
+ if (!tenantId) {
470
+ throw new Error("Phone login requires --corporation-id when multiple corporations are available.");
471
+ }
472
+ const tenantTicket = extractNextStepTicket(mobilePayload);
473
+ const tenantPayload = await postLoginForToken(loginOptions, fetchImpl, {
474
+ tenantID: tenantId,
475
+ ...(tenantTicket ? { ticket: tenantTicket } : {}),
476
+ flowCode: tenantStep.flowCode,
477
+ stepName: tenantStep.stepName
478
+ });
479
+ const profile = extractLoginProfile(tenantPayload, tenantId);
480
+ if (!profile.token || !profile.corporationId) {
481
+ throw new Error("Phone login response did not contain a token or corporation id.");
482
+ }
483
+ return withReportContext(profileFromLoginOptions(loginOptions, {
305
484
  token: profile.token,
306
485
  corporationId: profile.corporationId,
307
486
  languageCode: options.languageCode,
308
487
  timezone: options.timezone
309
- }, { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
488
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
310
489
  }
311
490
  async function loginWithExplicitPasswordFlow(options, fetchImpl) {
312
491
  const body = {
@@ -324,20 +503,19 @@ async function loginWithExplicitPasswordFlow(options, fetchImpl) {
324
503
  body.stepName = options.loginStepName;
325
504
  }
326
505
  const payload = await postLoginForToken(options, fetchImpl, body);
327
- const extracted = extractPasswordLoginProfile(payload, options.corporationId);
506
+ const extracted = extractLoginProfile(payload, options.corporationId);
328
507
  if (!extracted.token) {
329
508
  throw new Error(extractLoginError(payload) ?? "Password login response did not contain a token.");
330
509
  }
331
510
  if (!extracted.corporationId) {
332
511
  throw new Error("Password login response did not contain a corporation id.");
333
512
  }
334
- return withReportContext({
335
- baseUrl: options.baseUrl,
513
+ return withReportContext(profileFromLoginOptions(options, {
336
514
  token: extracted.token,
337
515
  corporationId: extracted.corporationId,
338
516
  languageCode: options.languageCode,
339
517
  timezone: options.timezone
340
- }, { ...options, employeeCode: options.employeeCode ?? extracted.employeeCode });
518
+ }), { ...options, employeeCode: options.employeeCode ?? extracted.employeeCode });
341
519
  }
342
520
  async function question(input, output, label, defaultValue) {
343
521
  const suffix = defaultValue ? ` (${defaultValue})` : "";
@@ -475,6 +653,7 @@ export function createTerminalPrompt(input = process.stdin, output = process.std
475
653
  }
476
654
  },
477
655
  secret: (label) => readLine(label),
656
+ message: (message) => output.write(`${message}\n`),
478
657
  close
479
658
  };
480
659
  }
@@ -513,34 +692,47 @@ export function createTerminalPrompt(input = process.stdin, output = process.std
513
692
  }
514
693
  return ask(label);
515
694
  },
695
+ message: (message) => output.write(`${message}\n`),
516
696
  close
517
697
  };
518
698
  }
519
- async function collectInteractiveLoginOptions(prompt, rawOptions) {
699
+ async function collectInteractiveLoginOptions(prompt, rawOptions, fetchImpl) {
520
700
  try {
521
- const baseUrl = (await prompt.text("BO URL", { defaultValue: DEFAULT_CHINA_BASE_URL })) || DEFAULT_CHINA_BASE_URL;
701
+ const baseUrl = (await prompt.text("CLI Gateway URL", { defaultValue: DEFAULT_AUTH_GATEWAY_BASE_URL })) || DEFAULT_AUTH_GATEWAY_BASE_URL;
522
702
  const corporationId = await prompt.text("集团 ID");
523
703
  const method = await prompt.select("鉴权方式", [
524
- { value: "password", label: "账号密码" },
525
- { value: "token", label: "Token" }
704
+ { value: "phone", label: "手机号验证码" },
705
+ { value: "password", label: "账号密码" }
526
706
  ]);
527
- if (method === "token") {
528
- return {
707
+ if (method === "phone") {
708
+ const phoneNumber = await prompt.text("手机号");
709
+ const phoneAreaCode = await prompt.text("手机区号", { defaultValue: DEFAULT_PHONE_AREA_CODE });
710
+ const phoneRegion = await prompt.text("手机地区", { defaultValue: DEFAULT_PHONE_REGION });
711
+ const phoneOptions = {
529
712
  ...rawOptions,
530
713
  baseUrl,
714
+ plat: rawOptions.plat ?? DEFAULT_AUTH_GATEWAY_PLAT,
531
715
  corporationId,
532
- token: await prompt.secret("Token"),
533
716
  username: undefined,
534
- password: undefined
717
+ password: undefined,
718
+ phoneNumber,
719
+ phoneAreaCode,
720
+ phoneRegion
721
+ };
722
+ return {
723
+ ...phoneOptions,
724
+ dynamicCode: await collectInteractivePhoneDynamicCode(prompt, phoneOptions, fetchImpl)
535
725
  };
536
726
  }
537
727
  return {
538
728
  ...rawOptions,
539
729
  baseUrl,
730
+ plat: rawOptions.plat ?? DEFAULT_AUTH_GATEWAY_PLAT,
540
731
  corporationId,
541
732
  username: await prompt.text("账号"),
542
733
  password: await prompt.secret("密码"),
543
- token: undefined
734
+ phoneNumber: undefined,
735
+ dynamicCode: undefined
544
736
  };
545
737
  }
546
738
  finally {
@@ -548,27 +740,29 @@ async function collectInteractiveLoginOptions(prompt, rawOptions) {
548
740
  }
549
741
  }
550
742
  async function saveLoginProfile(rawOptions, store, fetchImpl, write) {
551
- const hasToken = Boolean(rawOptions.token);
552
743
  const hasUsernamePassword = Boolean(rawOptions.username || rawOptions.password);
744
+ const hasPhoneLogin = Boolean(rawOptions.phoneNumber || rawOptions.dynamicCode);
553
745
  if (!rawOptions.baseUrl) {
554
746
  failJson(write, "INVALID_ARGUMENTS", "Login requires --base-url or --interactive.");
555
747
  }
556
- if (hasToken && hasUsernamePassword) {
557
- failJson(write, "INVALID_ARGUMENTS", "Use either --token or --username/--password, not both.");
748
+ if (hasPhoneLogin && hasUsernamePassword) {
749
+ failJson(write, "INVALID_ARGUMENTS", "Use either phone/dynamic-code or username/password, not both.");
558
750
  }
559
- if (hasToken) {
751
+ if (hasPhoneLogin) {
560
752
  if (!rawOptions.corporationId) {
561
- failJson(write, "INVALID_ARGUMENTS", "Token login requires --corporation-id.");
753
+ failJson(write, "INVALID_ARGUMENTS", "Phone login requires --corporation-id.");
754
+ }
755
+ if (!rawOptions.phoneNumber || !rawOptions.dynamicCode) {
756
+ failJson(write, "INVALID_ARGUMENTS", "Phone login requires --phone-number and --dynamic-code.");
757
+ }
758
+ try {
759
+ const profile = await loginWithPhone(rawOptions, fetchImpl);
760
+ store.save(await hydrateReportContext(profile, rawOptions, fetchImpl));
761
+ printJson(write, { authenticated: true, loginMethod: "phone" });
762
+ }
763
+ catch (error) {
764
+ failJson(write, "AUTH_LOGIN_FAILED", error instanceof Error ? error.message : "Phone login failed.");
562
765
  }
563
- const profile = await hydrateReportContext({
564
- baseUrl: rawOptions.baseUrl,
565
- token: rawOptions.token,
566
- corporationId: rawOptions.corporationId,
567
- languageCode: rawOptions.languageCode,
568
- timezone: rawOptions.timezone
569
- }, rawOptions, fetchImpl);
570
- store.save(profile);
571
- printJson(write, { authenticated: true, loginMethod: "token" });
572
766
  return;
573
767
  }
574
768
  if (!rawOptions.username || !rawOptions.password) {
@@ -592,24 +786,35 @@ export function createAuthCommand(options = {}) {
592
786
  .command("login")
593
787
  .description("Save local authentication profile")
594
788
  .option("--interactive", "Prompt for authentication details")
595
- .option("--base-url <url>", "Resto BO base URL")
596
- .option("--token <token>", "API token")
789
+ .option("--base-url <url>", "Resto auth gateway base URL")
790
+ .option("--plat <plat>", "Auth gateway platform code", DEFAULT_AUTH_GATEWAY_PLAT)
597
791
  .option("--corporation-id <id>", "Corporation ID")
598
- .option("--username <username>", "BO username/account for password login")
599
- .option("--password <password>", "BO password for password login")
792
+ .option("--username <username>", "Auth gateway username/account for password login")
793
+ .option("--password <password>", "Auth gateway password for password login")
794
+ .option("--phone-number <phone>", "Auth gateway phone number for dynamic-code login")
795
+ .option("--phone-area-code <code>", "Phone area code for dynamic-code login")
796
+ .option("--phone-region <region>", "Phone region for dynamic-code login")
797
+ .option("--dynamic-code <code>", "Phone dynamic verification code")
600
798
  .option("--employee-code <code>", "Employee code used by report permission context")
601
799
  .option("--organization-id <id>", "Current organization ID used by report permission context")
602
800
  .option("--organization-type <type>", "Current Organization-Type header used by report permission context")
603
801
  .option("--org-type-list <list>", "Comma-separated organization type list used by report permission context")
604
- .option("--login-flow-code <code>", "BO password login flowCode")
605
- .option("--login-step-name <name>", "BO password login stepName")
802
+ .option("--login-flow-code <code>", "Auth gateway password login flowCode")
803
+ .option("--login-step-name <name>", "Auth gateway password login stepName")
606
804
  .option("--language-code <code>", "Language code", DEFAULT_CHINA_LANGUAGE_CODE)
607
805
  .option("--timezone <timezone>", "Timezone", DEFAULT_CHINA_TIMEZONE)
608
806
  .allowExcessArguments(false)
609
807
  .action(async (rawOptions) => {
610
- const loginOptions = rawOptions.interactive
611
- ? await collectInteractiveLoginOptions(options.prompt ?? createTerminalPrompt(), rawOptions)
612
- : rawOptions;
808
+ let loginOptions;
809
+ try {
810
+ loginOptions = rawOptions.interactive
811
+ ? await collectInteractiveLoginOptions(options.prompt ?? createTerminalPrompt(), rawOptions, fetchImpl)
812
+ : rawOptions;
813
+ }
814
+ catch (error) {
815
+ failJson(write, "AUTH_LOGIN_FAILED", error instanceof Error ? error.message : "Interactive login failed.");
816
+ return;
817
+ }
613
818
  await saveLoginProfile(loginOptions, store, fetchImpl, write);
614
819
  });
615
820
  auth
@@ -625,6 +830,7 @@ export function createAuthCommand(options = {}) {
625
830
  printJson(write, {
626
831
  authenticated: true,
627
832
  baseUrl: profile.baseUrl,
833
+ ...(profile.plat ? { plat: profile.plat } : {}),
628
834
  corporationId: profile.corporationId,
629
835
  languageCode: profile.languageCode,
630
836
  timezone: profile.timezone,