@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.
- package/.agents/skills/resto-datacli/SKILL.md +32 -2
- package/README.md +44 -8
- package/dist/cli.js +2 -0
- package/dist/commands/auth.js +286 -80
- package/dist/commands/knowledge.js +32 -13
- package/dist/commands/order.js +109 -0
- package/dist/commands/public-records.js +32 -0
- package/dist/commands/query.js +22 -4
- package/dist/commands/shop.js +66 -14
- package/dist/config/profile.js +1 -0
- package/dist/knowledge/local-store.js +2 -0
- package/dist/knowledge/manifest-client.js +52 -3
- package/dist/knowledge/schema.js +2 -0
- package/dist/knowledge/search-index.js +46 -0
- package/dist/report-api/query-plan.js +20 -4
- package/dist/report-api/report-api-client.js +105 -15
- package/docs/installation.md +44 -14
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
|
10
|
-
const
|
|
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
|
|
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
|
|
94
|
+
function encryptGatewayPassword(password) {
|
|
58
95
|
return publicEncrypt({
|
|
59
|
-
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
|
|
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
|
|
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(
|
|
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("
|
|
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
|
|
202
|
+
return await loginWithGatewayPasswordSteps(loginOptions, fetchImpl);
|
|
182
203
|
}
|
|
183
204
|
async function postLoginForToken(options, fetchImpl, body) {
|
|
184
|
-
const response = await fetchImpl(
|
|
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(`
|
|
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) ?? `
|
|
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(
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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("
|
|
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: "
|
|
525
|
-
{ value: "
|
|
704
|
+
{ value: "phone", label: "手机号验证码" },
|
|
705
|
+
{ value: "password", label: "账号密码" }
|
|
526
706
|
]);
|
|
527
|
-
if (method === "
|
|
528
|
-
|
|
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
|
-
|
|
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 (
|
|
557
|
-
failJson(write, "INVALID_ARGUMENTS", "Use either
|
|
748
|
+
if (hasPhoneLogin && hasUsernamePassword) {
|
|
749
|
+
failJson(write, "INVALID_ARGUMENTS", "Use either phone/dynamic-code or username/password, not both.");
|
|
558
750
|
}
|
|
559
|
-
if (
|
|
751
|
+
if (hasPhoneLogin) {
|
|
560
752
|
if (!rawOptions.corporationId) {
|
|
561
|
-
failJson(write, "INVALID_ARGUMENTS", "
|
|
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
|
|
596
|
-
.option("--
|
|
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>", "
|
|
599
|
-
.option("--password <password>", "
|
|
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>", "
|
|
605
|
-
.option("--login-step-name <name>", "
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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,
|