@restoai/resto-datacli 0.1.12 → 0.2.1

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,66 @@ 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";
14
+ const ORGANIZATION_VIEW_PATH = "/vulcan/employee/queryOrgView";
12
15
  const REPORT_SHOP_ORG_TYPE_LIST = [7];
16
+ const ORGANIZATION_ID_FIELDS = [
17
+ "organizationId",
18
+ "orgId",
19
+ "corporationOrgId",
20
+ "brandId",
21
+ "orgShopId",
22
+ "franchiseeOrgId",
23
+ "orgFranchiseeId"
24
+ ];
25
+ const PHONE_CAPTCHA_TYPE_VERIFY_CODE = "1";
26
+ const PHONE_CAPTCHA_STEP_LOGIN = "1";
27
+ const DEFAULT_PHONE_AREA_CODE = "86";
28
+ const DEFAULT_PHONE_REGION = "CN";
29
+ const STEP_CHECK_MOBILE = "StepCheckMobile";
13
30
  const STEP_CHECK_EMAIL = "StepCheckEmail";
14
31
  const STEP_EMAIL = "StepEmail";
32
+ const STEP_MOBILE = "StepMobile";
15
33
  const STEP_SELECT_TENANT = "StepSelectTenant";
16
- const BO_PASSWORD_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
34
+ const AUTH_GATEWAY_PASSWORD_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
17
35
  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
36
  -----END PUBLIC KEY-----`;
19
37
  function trimTrailingSlash(value) {
20
38
  return value.replace(/\/+$/, "");
21
39
  }
40
+ function normalizeOptionalString(value) {
41
+ const trimmed = value?.trim();
42
+ return trimmed ? trimmed : undefined;
43
+ }
44
+ function appendQueryParam(url, key, value) {
45
+ const normalizedValue = normalizeOptionalString(value);
46
+ if (!normalizedValue) {
47
+ return url;
48
+ }
49
+ const hashIndex = url.indexOf("#");
50
+ const base = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
51
+ const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
52
+ if (new RegExp(`(^|[?&])${key}=`).test(base)) {
53
+ return url;
54
+ }
55
+ const separator = base.includes("?") ? "&" : "?";
56
+ return `${base}${separator}${encodeURIComponent(key)}=${encodeURIComponent(normalizedValue)}${hash}`;
57
+ }
58
+ function authGatewayUrl(baseUrl, path, plat) {
59
+ return appendQueryParam(`${trimTrailingSlash(baseUrl)}${path}`, "plat", plat);
60
+ }
61
+ function profileFromLoginOptions(options, profile) {
62
+ const plat = normalizeOptionalString(options.plat);
63
+ return {
64
+ baseUrl: options.baseUrl,
65
+ ...(plat ? { plat } : {}),
66
+ ...profile
67
+ };
68
+ }
22
69
  function asRecord(value) {
23
70
  return value && typeof value === "object" ? value : undefined;
24
71
  }
@@ -54,9 +101,9 @@ function extractLoginError(payload) {
54
101
  const root = asRecord(payload);
55
102
  return firstString(root?.msg, root?.message, root?.errorMessage);
56
103
  }
57
- function encryptBoPassword(password) {
104
+ function encryptGatewayPassword(password) {
58
105
  return publicEncrypt({
59
- key: BO_PASSWORD_PUBLIC_KEY,
106
+ key: AUTH_GATEWAY_PASSWORD_PUBLIC_KEY,
60
107
  padding: cryptoConstants.RSA_PKCS1_OAEP_PADDING,
61
108
  oaepHash: "sha256"
62
109
  }, Buffer.from(password)).toString("base64");
@@ -72,22 +119,6 @@ function parseIntegerList(value) {
72
119
  const numbers = parts.map((part) => Number.parseInt(part, 10));
73
120
  return numbers.every((number) => Number.isInteger(number) && number > 0) ? numbers : undefined;
74
121
  }
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
122
  function hasCompleteReportContext(reportContext) {
92
123
  return Boolean(reportContext?.employeeCode &&
93
124
  reportContext.organizationId &&
@@ -97,7 +128,7 @@ function hasCompleteReportContext(reportContext) {
97
128
  }
98
129
  function buildReportContext(profile, options) {
99
130
  const orgTypeList = options.orgTypeList ? parseIntegerList(options.orgTypeList) : profile.reportContext?.orgTypeList;
100
- const employeeCode = options.employeeCode ?? profile.reportContext?.employeeCode ?? inferEmployeeCodeFromToken(profile.token);
131
+ const employeeCode = options.employeeCode ?? profile.reportContext?.employeeCode;
101
132
  const organizationId = options.organizationId ?? profile.reportContext?.organizationId;
102
133
  const organizationType = options.organizationType ?? profile.reportContext?.organizationType;
103
134
  const reportContext = {
@@ -108,20 +139,41 @@ function buildReportContext(profile, options) {
108
139
  };
109
140
  return Object.keys(reportContext).length > 0 ? reportContext : undefined;
110
141
  }
111
- function extractProfileReportContext(payload) {
142
+ function extractProfileIdentity(payload) {
112
143
  const data = asRecord(asRecord(payload)?.data) ?? asRecord(payload);
113
144
  const employeeCode = firstString(data?.employeeCode, data?.code, data?.loginId, data?.principalUid);
145
+ const employeeId = firstString(data?.employeeId, data?.id, data?.userId);
114
146
  const organizationId = firstString(data?.organizationId, data?.orgId);
115
- const organizationType = firstString(data?.organizationType, data?.orgType, data?.type);
147
+ const organizationType = firstString(data?.organizationType, data?.orgType);
148
+ return { employeeCode, employeeId, organizationId, organizationType };
149
+ }
150
+ function extractProfileReportContext(payload, resolvedOrganizationType) {
151
+ const identity = extractProfileIdentity(payload);
152
+ const organizationType = resolvedOrganizationType ?? identity.organizationType;
116
153
  const reportContext = {
117
- ...(employeeCode ? { employeeCode } : {}),
118
- ...(organizationId ? { organizationId } : {}),
154
+ ...(identity.employeeCode ? { employeeCode: identity.employeeCode } : {}),
155
+ ...(identity.organizationId ? { organizationId: identity.organizationId } : {}),
119
156
  ...(organizationType ? { organizationType } : {}),
120
- ...(organizationId ? { orgTypeList: REPORT_SHOP_ORG_TYPE_LIST } : {})
157
+ ...(identity.organizationId ? { orgTypeList: REPORT_SHOP_ORG_TYPE_LIST } : {})
121
158
  };
122
159
  return Object.keys(reportContext).length > 0 ? reportContext : undefined;
123
160
  }
124
- function extractPasswordLoginProfile(payload, fallbackCorporationId) {
161
+ function resolveOrganizationType(payload, organizationId) {
162
+ const data = asRecord(asRecord(payload)?.data) ?? asRecord(payload);
163
+ const matchingTypes = new Set();
164
+ for (const record of collectRecords(data)) {
165
+ const matchesOrganization = ORGANIZATION_ID_FIELDS.some((fieldName) => firstString(record[fieldName]) === organizationId);
166
+ if (!matchesOrganization) {
167
+ continue;
168
+ }
169
+ const organizationType = firstString(record.orgType, record.organizationType);
170
+ if (organizationType) {
171
+ matchingTypes.add(organizationType);
172
+ }
173
+ }
174
+ return matchingTypes.size === 1 ? [...matchingTypes][0] : undefined;
175
+ }
176
+ function extractLoginProfile(payload, fallbackCorporationId) {
125
177
  const records = collectRecords(payload);
126
178
  const token = records.reduce((current, record) => current ?? firstString(record.token, record.accessToken, record.access_token), undefined);
127
179
  const employeeCode = records.reduce((current, record) => current ?? firstString(record.employeeCode, record.loginId, record.principalUid), undefined);
@@ -133,8 +185,35 @@ function withReportContext(profile, options) {
133
185
  const reportContext = buildReportContext(profile, options);
134
186
  return reportContext ? { ...profile, reportContext } : profile;
135
187
  }
136
- async function fetchProfileReportContext(profile, fetchImpl) {
137
- const response = await fetchImpl(`${trimTrailingSlash(profile.baseUrl)}${PROFILE_INFO_PATH}`, {
188
+ async function fetchOrganizationType(profile, fetchImpl, employeeId, organizationId) {
189
+ const response = await fetchImpl(authGatewayUrl(profile.baseUrl, ORGANIZATION_VIEW_PATH, profile.plat), {
190
+ method: "POST",
191
+ headers: {
192
+ Accept: "application/json",
193
+ "Content-Type": "application/json",
194
+ "Corporation-Id": profile.corporationId,
195
+ "Vulcan-Token": profile.token,
196
+ "Language-Code": profile.languageCode,
197
+ "Accept-Timezone": profile.timezone
198
+ },
199
+ body: JSON.stringify({ id: employeeId, corporationId: profile.corporationId })
200
+ });
201
+ if (!response.ok) {
202
+ throw new Error(`Organization context request failed: ${response.status} ${response.statusText}`.trim());
203
+ }
204
+ const payload = await response.json();
205
+ const code = firstString(asRecord(payload)?.code);
206
+ if (code && code !== "000") {
207
+ throw new Error(extractLoginError(payload) ?? `Organization context request failed with code ${code}.`);
208
+ }
209
+ const organizationType = resolveOrganizationType(payload, organizationId);
210
+ if (!organizationType) {
211
+ throw new Error("Current organization type could not be resolved from the organization view.");
212
+ }
213
+ return organizationType;
214
+ }
215
+ async function fetchProfileReportContext(profile, fetchImpl, organizationTypeOverride) {
216
+ const response = await fetchImpl(authGatewayUrl(profile.baseUrl, PROFILE_INFO_PATH, profile.plat), {
138
217
  method: "GET",
139
218
  headers: {
140
219
  Accept: "application/json",
@@ -145,43 +224,68 @@ async function fetchProfileReportContext(profile, fetchImpl) {
145
224
  }
146
225
  });
147
226
  if (!response.ok) {
148
- return undefined;
227
+ throw new Error(`Profile request failed: ${response.status} ${response.statusText}`.trim());
149
228
  }
150
229
  const payload = await response.json();
151
230
  const code = firstString(asRecord(payload)?.code);
152
231
  if (code && code !== "000") {
153
- return undefined;
232
+ throw new Error(extractLoginError(payload) ?? `Profile request failed with code ${code}.`);
233
+ }
234
+ const identity = extractProfileIdentity(payload);
235
+ let organizationType = organizationTypeOverride ?? identity.organizationType;
236
+ if (!organizationType && identity.organizationId) {
237
+ if (!identity.employeeId) {
238
+ throw new Error("Profile did not contain an employee id for organization resolution.");
239
+ }
240
+ organizationType = await fetchOrganizationType(profile, fetchImpl, identity.employeeId, identity.organizationId);
154
241
  }
155
- return extractProfileReportContext(payload);
242
+ return extractProfileReportContext(payload, organizationType);
156
243
  }
157
244
  async function hydrateReportContext(profile, options, fetchImpl) {
158
245
  const initialProfile = withReportContext(profile, options);
159
246
  if (hasCompleteReportContext(initialProfile.reportContext)) {
160
247
  return initialProfile;
161
248
  }
162
- try {
163
- const profileReportContext = await fetchProfileReportContext(profile, fetchImpl);
164
- const hydratedProfile = profileReportContext
165
- ? { ...profile, reportContext: { ...(profile.reportContext ?? {}), ...profileReportContext } }
166
- : profile;
167
- return withReportContext(hydratedProfile, options);
168
- }
169
- catch {
170
- return initialProfile;
171
- }
249
+ const profileReportContext = await fetchProfileReportContext(profile, fetchImpl, options.organizationType);
250
+ const hydratedProfile = profileReportContext
251
+ ? { ...profile, reportContext: { ...(profile.reportContext ?? {}), ...profileReportContext } }
252
+ : profile;
253
+ return withReportContext(hydratedProfile, options);
172
254
  }
173
255
  async function loginWithPassword(options, fetchImpl) {
174
256
  if (!options.baseUrl) {
175
- throw new Error("Password login requires --base-url.");
257
+ throw new Error("Username/password login requires --base-url.");
176
258
  }
177
259
  const loginOptions = { ...options, baseUrl: options.baseUrl };
178
260
  if (options.loginFlowCode || options.loginStepName) {
179
261
  return await loginWithExplicitPasswordFlow(loginOptions, fetchImpl);
180
262
  }
181
- return await loginWithBoPasswordSteps(loginOptions, fetchImpl);
263
+ return await loginWithGatewayPasswordSteps(loginOptions, fetchImpl);
182
264
  }
183
265
  async function postLoginForToken(options, fetchImpl, body) {
184
- const response = await fetchImpl(`${trimTrailingSlash(options.baseUrl)}${PASSWORD_LOGIN_PATH}`, {
266
+ const response = await fetchImpl(authGatewayUrl(options.baseUrl, LOGIN_FOR_TOKEN_PATH, options.plat), {
267
+ method: "POST",
268
+ headers: {
269
+ Accept: "application/json",
270
+ "Content-Type": "application/json",
271
+ "Language-Code": options.languageCode,
272
+ "Accept-Timezone": options.timezone
273
+ },
274
+ body: JSON.stringify(body)
275
+ });
276
+ const payload = await response.json();
277
+ if (!response.ok) {
278
+ throw new Error(`Login request failed: ${response.status} ${response.statusText}`.trim());
279
+ }
280
+ const root = asRecord(payload);
281
+ const code = firstString(root?.code);
282
+ if (code && code !== "000") {
283
+ throw new Error(extractLoginError(payload) ?? `Login failed with code ${code}.`);
284
+ }
285
+ return payload;
286
+ }
287
+ async function postPhoneCaptcha(options, fetchImpl, body) {
288
+ const response = await fetchImpl(authGatewayUrl(options.baseUrl, PHONE_CAPTCHA_PATH, options.plat), {
185
289
  method: "POST",
186
290
  headers: {
187
291
  Accept: "application/json",
@@ -193,15 +297,80 @@ async function postLoginForToken(options, fetchImpl, body) {
193
297
  });
194
298
  const payload = await response.json();
195
299
  if (!response.ok) {
196
- throw new Error(`Password login request failed: ${response.status} ${response.statusText}`.trim());
300
+ throw new Error(`Phone dynamic code request failed: ${response.status} ${response.statusText}`.trim());
197
301
  }
198
302
  const root = asRecord(payload);
199
303
  const code = firstString(root?.code);
200
304
  if (code && code !== "000") {
201
- throw new Error(extractLoginError(payload) ?? `Password login failed with code ${code}.`);
305
+ throw new Error(extractLoginError(payload) ?? `Phone dynamic code request failed with code ${code}.`);
202
306
  }
203
307
  return payload;
204
308
  }
309
+ function normalizePhoneAreaCode(value) {
310
+ return normalizeOptionalString(value) ?? DEFAULT_PHONE_AREA_CODE;
311
+ }
312
+ function normalizePhoneRegion(value) {
313
+ return normalizeOptionalString(value) ?? DEFAULT_PHONE_REGION;
314
+ }
315
+ function maskPhoneNumber(phoneNumber) {
316
+ const trimmed = phoneNumber.trim();
317
+ return trimmed.length > 7 ? `${trimmed.slice(0, 3)}****${trimmed.slice(-4)}` : trimmed;
318
+ }
319
+ function formatPhoneCodeSendError(error) {
320
+ const message = error instanceof Error ? error.message : "验证码发送失败";
321
+ return message.replace(/^Phone dynamic code request failed:\s*/, "").trim() || "验证码发送失败";
322
+ }
323
+ function buildPhoneIdentity(options) {
324
+ const phoneNumber = normalizeOptionalString(options.phoneNumber);
325
+ if (!phoneNumber) {
326
+ throw new Error("Phone login requires --phone-number.");
327
+ }
328
+ return {
329
+ phoneAreaCode: normalizePhoneAreaCode(options.phoneAreaCode),
330
+ phoneNumber,
331
+ phoneRegion: normalizePhoneRegion(options.phoneRegion)
332
+ };
333
+ }
334
+ async function sendPhoneDynamicCode(options, fetchImpl) {
335
+ if (!options.corporationId) {
336
+ throw new Error("Phone login requires --corporation-id before sending a dynamic code.");
337
+ }
338
+ const phone = buildPhoneIdentity(options);
339
+ await postPhoneCaptcha(options, fetchImpl, {
340
+ corporationId: options.corporationId,
341
+ captchaType: PHONE_CAPTCHA_TYPE_VERIFY_CODE,
342
+ step: PHONE_CAPTCHA_STEP_LOGIN,
343
+ phone: phone.phoneNumber,
344
+ phoneAreaCode: phone.phoneAreaCode,
345
+ region: phone.phoneRegion
346
+ });
347
+ }
348
+ async function collectInteractivePhoneDynamicCode(prompt, phoneOptions, fetchImpl) {
349
+ let action = await prompt.select(`验证码发送到 ${maskPhoneNumber(phoneOptions.phoneNumber ?? "")}`, [
350
+ { value: "send", label: "发送验证码" },
351
+ { value: "manual", label: "我已获取验证码,继续输入" },
352
+ { value: "cancel", label: "取消登录" }
353
+ ]);
354
+ while (action === "send" || action === "retry") {
355
+ try {
356
+ await sendPhoneDynamicCode(phoneOptions, fetchImpl);
357
+ prompt.message?.("验证码已发送,请输入短信验证码。");
358
+ break;
359
+ }
360
+ catch (error) {
361
+ prompt.message?.(`发送验证码失败:${formatPhoneCodeSendError(error)}`);
362
+ action = await prompt.select("验证码发送失败", [
363
+ { value: "retry", label: "重试发送" },
364
+ { value: "manual", label: "我已获取验证码,继续输入" },
365
+ { value: "cancel", label: "取消登录" }
366
+ ]);
367
+ }
368
+ }
369
+ if (action === "cancel") {
370
+ throw new Error("Interactive login canceled.");
371
+ }
372
+ return await prompt.secret("验证码");
373
+ }
205
374
  function extractFirstNextStep(payload) {
206
375
  const data = asRecord(asRecord(payload)?.data);
207
376
  const nextSteps = data?.nextSteps;
@@ -220,7 +389,7 @@ function extractNextStepTicket(payload) {
220
389
  }
221
390
  function hasCompletedLogin(payload) {
222
391
  const data = asRecord(asRecord(payload)?.data);
223
- return data?.hasNextStep === false || Boolean(extractPasswordLoginProfile(payload).token);
392
+ return data?.hasNextStep === false || Boolean(extractLoginProfile(payload).token);
224
393
  }
225
394
  function inferSingleCorporationId(payload) {
226
395
  const corporationIds = new Set();
@@ -232,7 +401,7 @@ function inferSingleCorporationId(payload) {
232
401
  }
233
402
  return corporationIds.size === 1 ? [...corporationIds][0] : undefined;
234
403
  }
235
- async function loginWithBoPasswordSteps(options, fetchImpl) {
404
+ async function loginWithGatewayPasswordSteps(options, fetchImpl) {
236
405
  const email = options.username ?? "";
237
406
  const password = options.password ?? "";
238
407
  const checkEmailPayload = await postLoginForToken(options, fetchImpl, {
@@ -241,17 +410,16 @@ async function loginWithBoPasswordSteps(options, fetchImpl) {
241
410
  stepName: STEP_CHECK_EMAIL
242
411
  });
243
412
  if (hasCompletedLogin(checkEmailPayload)) {
244
- const profile = extractPasswordLoginProfile(checkEmailPayload, options.corporationId);
413
+ const profile = extractLoginProfile(checkEmailPayload, options.corporationId);
245
414
  if (!profile.token || !profile.corporationId) {
246
415
  throw new Error("Password login response did not contain a token or corporation id.");
247
416
  }
248
- return withReportContext({
249
- baseUrl: options.baseUrl,
417
+ return withReportContext(profileFromLoginOptions(options, {
250
418
  token: profile.token,
251
419
  corporationId: profile.corporationId,
252
420
  languageCode: options.languageCode,
253
421
  timezone: options.timezone
254
- }, { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
422
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
255
423
  }
256
424
  const emailStep = extractFirstNextStep(checkEmailPayload);
257
425
  const emailTicket = extractNextStepTicket(checkEmailPayload);
@@ -260,23 +428,22 @@ async function loginWithBoPasswordSteps(options, fetchImpl) {
260
428
  }
261
429
  const passwordPayload = await postLoginForToken(options, fetchImpl, {
262
430
  email,
263
- password: encryptBoPassword(password),
431
+ password: encryptGatewayPassword(password),
264
432
  ticket: emailTicket,
265
433
  flowCode: emailStep.flowCode,
266
434
  stepName: emailStep.stepName
267
435
  });
268
436
  if (hasCompletedLogin(passwordPayload)) {
269
- const profile = extractPasswordLoginProfile(passwordPayload, options.corporationId);
437
+ const profile = extractLoginProfile(passwordPayload, options.corporationId);
270
438
  if (!profile.token || !profile.corporationId) {
271
439
  throw new Error("Password login response did not contain a token or corporation id.");
272
440
  }
273
- return withReportContext({
274
- baseUrl: options.baseUrl,
441
+ return withReportContext(profileFromLoginOptions(options, {
275
442
  token: profile.token,
276
443
  corporationId: profile.corporationId,
277
444
  languageCode: options.languageCode,
278
445
  timezone: options.timezone
279
- }, { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
446
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
280
447
  }
281
448
  const tenantStep = extractFirstNextStep(passwordPayload);
282
449
  if (!tenantStep || tenantStep.stepName !== STEP_SELECT_TENANT) {
@@ -296,17 +463,90 @@ async function loginWithBoPasswordSteps(options, fetchImpl) {
296
463
  flowCode: tenantStep.flowCode,
297
464
  stepName: tenantStep.stepName
298
465
  });
299
- const profile = extractPasswordLoginProfile(tenantPayload, tenantId);
466
+ const profile = extractLoginProfile(tenantPayload, tenantId);
300
467
  if (!profile.token || !profile.corporationId) {
301
468
  throw new Error("Password login response did not contain a token or corporation id.");
302
469
  }
303
- return withReportContext({
304
- baseUrl: options.baseUrl,
470
+ return withReportContext(profileFromLoginOptions(options, {
305
471
  token: profile.token,
306
472
  corporationId: profile.corporationId,
307
473
  languageCode: options.languageCode,
308
474
  timezone: options.timezone
309
- }, { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
475
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
476
+ }
477
+ async function loginWithPhone(options, fetchImpl) {
478
+ if (!options.baseUrl) {
479
+ throw new Error("Phone login requires --base-url.");
480
+ }
481
+ if (!options.dynamicCode) {
482
+ throw new Error("Phone login requires --dynamic-code.");
483
+ }
484
+ const loginOptions = { ...options, baseUrl: options.baseUrl };
485
+ const phone = buildPhoneIdentity(options);
486
+ const checkMobilePayload = await postLoginForToken(loginOptions, fetchImpl, {
487
+ ...phone,
488
+ flowCode: STEP_CHECK_MOBILE,
489
+ stepName: STEP_CHECK_MOBILE
490
+ });
491
+ if (hasCompletedLogin(checkMobilePayload)) {
492
+ const profile = extractLoginProfile(checkMobilePayload, options.corporationId);
493
+ if (!profile.token || !profile.corporationId) {
494
+ throw new Error("Phone login response did not contain a token or corporation id.");
495
+ }
496
+ return withReportContext(profileFromLoginOptions(loginOptions, {
497
+ token: profile.token,
498
+ corporationId: profile.corporationId,
499
+ languageCode: options.languageCode,
500
+ timezone: options.timezone
501
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
502
+ }
503
+ const mobileStep = extractFirstNextStep(checkMobilePayload);
504
+ if (!mobileStep || mobileStep.stepName !== STEP_MOBILE) {
505
+ throw new Error("Phone login did not return the expected StepMobile step.");
506
+ }
507
+ const mobilePayload = await postLoginForToken(loginOptions, fetchImpl, {
508
+ ...phone,
509
+ dynamicCode: options.dynamicCode,
510
+ flowCode: mobileStep.flowCode,
511
+ stepName: mobileStep.stepName
512
+ });
513
+ if (hasCompletedLogin(mobilePayload)) {
514
+ const profile = extractLoginProfile(mobilePayload, options.corporationId);
515
+ if (!profile.token || !profile.corporationId) {
516
+ throw new Error("Phone login response did not contain a token or corporation id.");
517
+ }
518
+ return withReportContext(profileFromLoginOptions(loginOptions, {
519
+ token: profile.token,
520
+ corporationId: profile.corporationId,
521
+ languageCode: options.languageCode,
522
+ timezone: options.timezone
523
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
524
+ }
525
+ const tenantStep = extractFirstNextStep(mobilePayload);
526
+ if (!tenantStep || tenantStep.stepName !== STEP_SELECT_TENANT) {
527
+ throw new Error("Phone login returned an unsupported next step.");
528
+ }
529
+ const tenantId = options.corporationId ?? inferSingleCorporationId(mobilePayload);
530
+ if (!tenantId) {
531
+ throw new Error("Phone login requires --corporation-id when multiple corporations are available.");
532
+ }
533
+ const tenantTicket = extractNextStepTicket(mobilePayload);
534
+ const tenantPayload = await postLoginForToken(loginOptions, fetchImpl, {
535
+ tenantID: tenantId,
536
+ ...(tenantTicket ? { ticket: tenantTicket } : {}),
537
+ flowCode: tenantStep.flowCode,
538
+ stepName: tenantStep.stepName
539
+ });
540
+ const profile = extractLoginProfile(tenantPayload, tenantId);
541
+ if (!profile.token || !profile.corporationId) {
542
+ throw new Error("Phone login response did not contain a token or corporation id.");
543
+ }
544
+ return withReportContext(profileFromLoginOptions(loginOptions, {
545
+ token: profile.token,
546
+ corporationId: profile.corporationId,
547
+ languageCode: options.languageCode,
548
+ timezone: options.timezone
549
+ }), { ...options, employeeCode: options.employeeCode ?? profile.employeeCode });
310
550
  }
311
551
  async function loginWithExplicitPasswordFlow(options, fetchImpl) {
312
552
  const body = {
@@ -324,20 +564,19 @@ async function loginWithExplicitPasswordFlow(options, fetchImpl) {
324
564
  body.stepName = options.loginStepName;
325
565
  }
326
566
  const payload = await postLoginForToken(options, fetchImpl, body);
327
- const extracted = extractPasswordLoginProfile(payload, options.corporationId);
567
+ const extracted = extractLoginProfile(payload, options.corporationId);
328
568
  if (!extracted.token) {
329
569
  throw new Error(extractLoginError(payload) ?? "Password login response did not contain a token.");
330
570
  }
331
571
  if (!extracted.corporationId) {
332
572
  throw new Error("Password login response did not contain a corporation id.");
333
573
  }
334
- return withReportContext({
335
- baseUrl: options.baseUrl,
574
+ return withReportContext(profileFromLoginOptions(options, {
336
575
  token: extracted.token,
337
576
  corporationId: extracted.corporationId,
338
577
  languageCode: options.languageCode,
339
578
  timezone: options.timezone
340
- }, { ...options, employeeCode: options.employeeCode ?? extracted.employeeCode });
579
+ }), { ...options, employeeCode: options.employeeCode ?? extracted.employeeCode });
341
580
  }
342
581
  async function question(input, output, label, defaultValue) {
343
582
  const suffix = defaultValue ? ` (${defaultValue})` : "";
@@ -475,6 +714,7 @@ export function createTerminalPrompt(input = process.stdin, output = process.std
475
714
  }
476
715
  },
477
716
  secret: (label) => readLine(label),
717
+ message: (message) => output.write(`${message}\n`),
478
718
  close
479
719
  };
480
720
  }
@@ -513,34 +753,47 @@ export function createTerminalPrompt(input = process.stdin, output = process.std
513
753
  }
514
754
  return ask(label);
515
755
  },
756
+ message: (message) => output.write(`${message}\n`),
516
757
  close
517
758
  };
518
759
  }
519
- async function collectInteractiveLoginOptions(prompt, rawOptions) {
760
+ async function collectInteractiveLoginOptions(prompt, rawOptions, fetchImpl) {
520
761
  try {
521
- const baseUrl = (await prompt.text("BO URL", { defaultValue: DEFAULT_CHINA_BASE_URL })) || DEFAULT_CHINA_BASE_URL;
762
+ const baseUrl = (await prompt.text("CLI Gateway URL", { defaultValue: DEFAULT_AUTH_GATEWAY_BASE_URL })) || DEFAULT_AUTH_GATEWAY_BASE_URL;
522
763
  const corporationId = await prompt.text("集团 ID");
523
764
  const method = await prompt.select("鉴权方式", [
524
- { value: "password", label: "账号密码" },
525
- { value: "token", label: "Token" }
765
+ { value: "phone", label: "手机号验证码" },
766
+ { value: "password", label: "账号密码" }
526
767
  ]);
527
- if (method === "token") {
528
- return {
768
+ if (method === "phone") {
769
+ const phoneNumber = await prompt.text("手机号");
770
+ const phoneAreaCode = await prompt.text("手机区号", { defaultValue: DEFAULT_PHONE_AREA_CODE });
771
+ const phoneRegion = await prompt.text("手机地区", { defaultValue: DEFAULT_PHONE_REGION });
772
+ const phoneOptions = {
529
773
  ...rawOptions,
530
774
  baseUrl,
775
+ plat: rawOptions.plat ?? DEFAULT_AUTH_GATEWAY_PLAT,
531
776
  corporationId,
532
- token: await prompt.secret("Token"),
533
777
  username: undefined,
534
- password: undefined
778
+ password: undefined,
779
+ phoneNumber,
780
+ phoneAreaCode,
781
+ phoneRegion
782
+ };
783
+ return {
784
+ ...phoneOptions,
785
+ dynamicCode: await collectInteractivePhoneDynamicCode(prompt, phoneOptions, fetchImpl)
535
786
  };
536
787
  }
537
788
  return {
538
789
  ...rawOptions,
539
790
  baseUrl,
791
+ plat: rawOptions.plat ?? DEFAULT_AUTH_GATEWAY_PLAT,
540
792
  corporationId,
541
793
  username: await prompt.text("账号"),
542
794
  password: await prompt.secret("密码"),
543
- token: undefined
795
+ phoneNumber: undefined,
796
+ dynamicCode: undefined
544
797
  };
545
798
  }
546
799
  finally {
@@ -548,27 +801,29 @@ async function collectInteractiveLoginOptions(prompt, rawOptions) {
548
801
  }
549
802
  }
550
803
  async function saveLoginProfile(rawOptions, store, fetchImpl, write) {
551
- const hasToken = Boolean(rawOptions.token);
552
804
  const hasUsernamePassword = Boolean(rawOptions.username || rawOptions.password);
805
+ const hasPhoneLogin = Boolean(rawOptions.phoneNumber || rawOptions.dynamicCode);
553
806
  if (!rawOptions.baseUrl) {
554
807
  failJson(write, "INVALID_ARGUMENTS", "Login requires --base-url or --interactive.");
555
808
  }
556
- if (hasToken && hasUsernamePassword) {
557
- failJson(write, "INVALID_ARGUMENTS", "Use either --token or --username/--password, not both.");
809
+ if (hasPhoneLogin && hasUsernamePassword) {
810
+ failJson(write, "INVALID_ARGUMENTS", "Use either phone/dynamic-code or username/password, not both.");
558
811
  }
559
- if (hasToken) {
812
+ if (hasPhoneLogin) {
560
813
  if (!rawOptions.corporationId) {
561
- failJson(write, "INVALID_ARGUMENTS", "Token login requires --corporation-id.");
814
+ failJson(write, "INVALID_ARGUMENTS", "Phone login requires --corporation-id.");
815
+ }
816
+ if (!rawOptions.phoneNumber || !rawOptions.dynamicCode) {
817
+ failJson(write, "INVALID_ARGUMENTS", "Phone login requires --phone-number and --dynamic-code.");
818
+ }
819
+ try {
820
+ const profile = await loginWithPhone(rawOptions, fetchImpl);
821
+ store.save(await hydrateReportContext(profile, rawOptions, fetchImpl));
822
+ printJson(write, { authenticated: true, loginMethod: "phone" });
823
+ }
824
+ catch (error) {
825
+ failJson(write, "AUTH_LOGIN_FAILED", error instanceof Error ? error.message : "Phone login failed.");
562
826
  }
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
827
  return;
573
828
  }
574
829
  if (!rawOptions.username || !rawOptions.password) {
@@ -592,24 +847,35 @@ export function createAuthCommand(options = {}) {
592
847
  .command("login")
593
848
  .description("Save local authentication profile")
594
849
  .option("--interactive", "Prompt for authentication details")
595
- .option("--base-url <url>", "Resto BO base URL")
596
- .option("--token <token>", "API token")
850
+ .option("--base-url <url>", "Resto auth gateway base URL")
851
+ .option("--plat <plat>", "Auth gateway platform code", DEFAULT_AUTH_GATEWAY_PLAT)
597
852
  .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")
853
+ .option("--username <username>", "Auth gateway username/account for password login")
854
+ .option("--password <password>", "Auth gateway password for password login")
855
+ .option("--phone-number <phone>", "Auth gateway phone number for dynamic-code login")
856
+ .option("--phone-area-code <code>", "Phone area code for dynamic-code login")
857
+ .option("--phone-region <region>", "Phone region for dynamic-code login")
858
+ .option("--dynamic-code <code>", "Phone dynamic verification code")
600
859
  .option("--employee-code <code>", "Employee code used by report permission context")
601
860
  .option("--organization-id <id>", "Current organization ID used by report permission context")
602
861
  .option("--organization-type <type>", "Current Organization-Type header used by report permission context")
603
862
  .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")
863
+ .option("--login-flow-code <code>", "Auth gateway password login flowCode")
864
+ .option("--login-step-name <name>", "Auth gateway password login stepName")
606
865
  .option("--language-code <code>", "Language code", DEFAULT_CHINA_LANGUAGE_CODE)
607
866
  .option("--timezone <timezone>", "Timezone", DEFAULT_CHINA_TIMEZONE)
608
867
  .allowExcessArguments(false)
609
868
  .action(async (rawOptions) => {
610
- const loginOptions = rawOptions.interactive
611
- ? await collectInteractiveLoginOptions(options.prompt ?? createTerminalPrompt(), rawOptions)
612
- : rawOptions;
869
+ let loginOptions;
870
+ try {
871
+ loginOptions = rawOptions.interactive
872
+ ? await collectInteractiveLoginOptions(options.prompt ?? createTerminalPrompt(), rawOptions, fetchImpl)
873
+ : rawOptions;
874
+ }
875
+ catch (error) {
876
+ failJson(write, "AUTH_LOGIN_FAILED", error instanceof Error ? error.message : "Interactive login failed.");
877
+ return;
878
+ }
613
879
  await saveLoginProfile(loginOptions, store, fetchImpl, write);
614
880
  });
615
881
  auth
@@ -625,6 +891,7 @@ export function createAuthCommand(options = {}) {
625
891
  printJson(write, {
626
892
  authenticated: true,
627
893
  baseUrl: profile.baseUrl,
894
+ ...(profile.plat ? { plat: profile.plat } : {}),
628
895
  corporationId: profile.corporationId,
629
896
  languageCode: profile.languageCode,
630
897
  timezone: profile.timezone,