@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.
- package/.agents/skills/resto-datacli/SKILL.md +21 -2
- package/README.md +44 -8
- package/dist/cli.js +2 -0
- package/dist/commands/auth.js +366 -99
- 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,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
|
|
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";
|
|
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
|
|
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
|
|
104
|
+
function encryptGatewayPassword(password) {
|
|
58
105
|
return publicEncrypt({
|
|
59
|
-
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
137
|
-
const response = await fetchImpl(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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("
|
|
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
|
|
263
|
+
return await loginWithGatewayPasswordSteps(loginOptions, fetchImpl);
|
|
182
264
|
}
|
|
183
265
|
async function postLoginForToken(options, fetchImpl, body) {
|
|
184
|
-
const response = await fetchImpl(
|
|
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(`
|
|
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) ?? `
|
|
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(
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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("
|
|
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: "
|
|
525
|
-
{ value: "
|
|
765
|
+
{ value: "phone", label: "手机号验证码" },
|
|
766
|
+
{ value: "password", label: "账号密码" }
|
|
526
767
|
]);
|
|
527
|
-
if (method === "
|
|
528
|
-
|
|
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
|
-
|
|
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 (
|
|
557
|
-
failJson(write, "INVALID_ARGUMENTS", "Use either
|
|
809
|
+
if (hasPhoneLogin && hasUsernamePassword) {
|
|
810
|
+
failJson(write, "INVALID_ARGUMENTS", "Use either phone/dynamic-code or username/password, not both.");
|
|
558
811
|
}
|
|
559
|
-
if (
|
|
812
|
+
if (hasPhoneLogin) {
|
|
560
813
|
if (!rawOptions.corporationId) {
|
|
561
|
-
failJson(write, "INVALID_ARGUMENTS", "
|
|
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
|
|
596
|
-
.option("--
|
|
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>", "
|
|
599
|
-
.option("--password <password>", "
|
|
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>", "
|
|
605
|
-
.option("--login-step-name <name>", "
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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,
|