@youzan-cloud/zan-saas-cli 0.1.0-beta.4 → 0.1.0-beta.8
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/bin/zan-saas-cli.js +8 -11
- package/internal/registry/manifest.json +25 -20
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/zan-saas/SKILL.md +3 -3
- package/skills/zan-saas/references/host-entry.md +8 -0
- package/skills/zan-saas-order/SKILL.md +12 -1
- package/skills/zan-saas-order/references/branch-counts.md +45 -0
- package/skills/zan-saas-order/references/order-get.md +2 -0
- package/skills/zan-saas-order/references/order-list.md +2 -0
- package/skills/zan-saas-order/references/order-scan.md +2 -0
- package/skills/zan-saas-org/SKILL.md +6 -0
- package/skills/zan-saas-org/references/unit-list.md +2 -0
- package/skills/zan-saas-shared/SKILL.md +20 -5
- package/skills/zan-saas-shared/references/authentication.md +9 -4
- package/skills/zan-saas-shared/references/gateway-errors.md +29 -0
- package/skills/zan-saas-shared/references/shop-context.md +39 -0
- package/skills/zan-saas-valuecard/SKILL.md +8 -2
- package/skills/zan-saas-valuecard/references/transaction-list.md +25 -0
- package/skills/zan-saas-valuecard/references/transaction-scan.md +23 -0
- package/src/auth/credential-source.js +26 -0
- package/src/auth/credential-store.js +61 -5
- package/src/auth/session.js +80 -27
- package/src/cli/command-paths.js +3 -2
- package/src/cli/context-help.js +3 -1
- package/src/cli/program.js +20 -4
- package/src/commands/native/access/commands.index.json +1 -1
- package/src/commands/native/access/role/list/command.js +4 -32
- package/src/commands/native/customer/commands.index.json +1 -1
- package/src/commands/native/customer/customer-tag/list/command.js +2 -2
- package/src/commands/native/member/commands.index.json +1 -1
- package/src/commands/native/order/commands.index.json +3 -3
- package/src/commands/native/order/order/get/command.js +14 -9
- package/src/commands/native/order/order/get/schema.json +197 -41
- package/src/commands/native/order/order/list/command.js +9 -6
- package/src/commands/native/order/order/list/schema.json +140 -10
- package/src/commands/native/order/order/scan/command.js +8 -5
- package/src/commands/native/order/order/scan/scan.js +1 -1
- package/src/commands/native/order/order/scan/schema.json +135 -8
- package/src/commands/native/order/shared/query.js +57 -9
- package/src/commands/native/order/shared/search.js +1 -1
- package/src/commands/native/org/commands.index.json +4 -4
- package/src/commands/native/valuecard/commands.index.json +20 -2
- package/src/commands/native/valuecard/domain.json +1 -1
- package/src/commands/native/valuecard/transaction/list/command.js +110 -0
- package/src/commands/native/valuecard/transaction/list/projection.js +71 -0
- package/src/commands/native/valuecard/transaction/list/schema.json +888 -0
- package/src/commands/native/valuecard/transaction/scan/command.js +130 -0
- package/src/commands/native/valuecard/transaction/scan/projection.js +58 -0
- package/src/commands/native/valuecard/transaction/scan/scan.js +70 -0
- package/src/commands/native/valuecard/transaction/scan/schema.json +954 -0
- package/src/commands/system/api.js +1 -1
- package/src/commands/system/auth.js +13 -2
- package/src/commands/system/contracts.js +27 -19
- package/src/commands/system/shop-contracts.js +97 -0
- package/src/commands/system/shop.js +128 -27
- package/src/execution/error-details.js +13 -18
- package/src/execution/gateway-guidance.js +43 -0
- package/src/execution/shop-query.js +6 -3
- package/src/identity/context-store.js +14 -10
- package/src/identity/file-store.js +147 -0
- package/src/identity/session-store.js +261 -0
- package/src/identity/shop-location.js +158 -0
- package/src/identity/unit-membership.js +42 -0
- package/src/transport/api.js +78 -11
- package/src/transport/shop-app.js +8 -1
|
@@ -32,7 +32,7 @@ function createApiCommand() {
|
|
|
32
32
|
.option('--yes', '确认本次请求的未知副作用并发送')
|
|
33
33
|
.addHelpText('after', `
|
|
34
34
|
目标可以是已登记或未登记的 API,不接收任意 URL。
|
|
35
|
-
|
|
35
|
+
默认优先使用 auth login 保存的个人凭证;无个人记录时使用环境token。明确使用环境身份配置 ZAN_SAAS_AUTH_SOURCE=env。
|
|
36
36
|
发送须 --yes,仅确认未知副作用,不证明服务端授权。
|
|
37
37
|
退出0、HTTP 2xx 和 sent=true 只说明收到响应,不证明业务成功、异步完成或写入完成。
|
|
38
38
|
body 是脱敏后的原始文本;即使 HTTP 200 正文含 success=false,也不按业务失败改变退出码。
|
|
@@ -19,23 +19,33 @@ function localTime(iso) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const cloudAccountHint = '有赞云账号开通未确认,不影响本次个人登录。请先核对账号接口状态。';
|
|
22
|
+
function credentialLine(value) {
|
|
23
|
+
if (value.credentialSource === 'login') {
|
|
24
|
+
return `默认使用个人登录凭证${value.environmentTokenIgnored ? ',已忽略环境 token' : ''}。`;
|
|
25
|
+
}
|
|
26
|
+
if (value.credentialSource === 'env') return '本次配置使用环境 token;个人登录状态与环境凭证有效性分别判断。';
|
|
27
|
+
return value.credentialSource === 'missing' ? '当前没有选中的请求凭证。' : '';
|
|
28
|
+
}
|
|
22
29
|
|
|
23
30
|
const text = {
|
|
24
31
|
login: value => {
|
|
25
32
|
const lines = [value.refreshExpiresAt
|
|
26
33
|
? `登录成功,凭证有效期至 ${localTime(value.refreshExpiresAt)}。` : '登录成功。'];
|
|
27
34
|
if (value.cloudAccount?.ensured === false) lines.push(cloudAccountHint);
|
|
35
|
+
if (credentialLine(value)) lines.push(credentialLine(value));
|
|
28
36
|
lines.push(value.shopId ? `当前店铺上下文:kdtId ${value.shopId}。` : '当前没有有效的店铺上下文。',
|
|
29
37
|
shopSelectionHint);
|
|
30
38
|
return lines.join('\n');
|
|
31
39
|
},
|
|
32
40
|
status: value => {
|
|
33
|
-
if (!value.loggedIn) return `未登录(${value.server})。执行 zan-saas auth login
|
|
41
|
+
if (!value.loggedIn) return `未登录(${value.server})。执行 zan-saas auth login 登录。` +
|
|
42
|
+
(credentialLine(value) ? `\n${credentialLine(value)}` : '');
|
|
34
43
|
const lines = [`已登录(${value.server})`];
|
|
35
44
|
if (value.loginAt) lines.push(`登录时间:${localTime(value.loginAt)}`);
|
|
36
45
|
if (value.refreshExpiresAt) lines.push(`有效期至:${localTime(value.refreshExpiresAt)}`);
|
|
37
46
|
if (value.shopId) lines.push(`当前店铺:${value.shopId}`);
|
|
38
47
|
else lines.push('当前没有有效的店铺上下文。');
|
|
48
|
+
if (credentialLine(value)) lines.push(credentialLine(value));
|
|
39
49
|
lines.push(shopSelectionHint);
|
|
40
50
|
return lines.join('\n');
|
|
41
51
|
},
|
|
@@ -83,7 +93,8 @@ function createAuthCommand(impl = defaults) {
|
|
|
83
93
|
--no-browser 只跳过自动打开浏览器,仍等待授权回调并保存个人凭证,默认超时5分钟。
|
|
84
94
|
人机交接:Agent 启动命令并把输出的登录地址交给用户;用户在浏览器确认后,Agent 等待本命令回调,再执行 zan-saas auth status 确认。用户取消或超时后重新执行本命令。
|
|
85
95
|
登录成功后执行 zan-saas shop current 查看当前店铺。
|
|
86
|
-
|
|
96
|
+
宿主会话首次使用执行 shop session init,恢复时加 --resume;账号选店、连锁组织切换及本次临时查询按共享指引分别处理。
|
|
97
|
+
默认优先使用已保存的个人凭证;仅受控调用明确配置 ZAN_SAAS_AUTH_SOURCE=env 时使用环境 token。
|
|
87
98
|
执行 zan-saas shop current 确认实际生效的店铺,再开始业务查询;切店无需重新登录。
|
|
88
99
|
登录后尝试开通有赞云账号,未确认开通时不影响本次个人登录,应核对账号接口状态。
|
|
89
100
|
浏览器授权完成后仍须成功换取并保存凭证;本命令失败时停止,不进入选店或业务查询。
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { attachCommandSchema } = require('../../execution/command-schema');
|
|
4
4
|
const { parserErrorModel } = require('../../execution/parser-error');
|
|
5
|
+
const shopModels = require('./shop-contracts');
|
|
5
6
|
|
|
6
7
|
function object(properties, required = Object.keys(properties)) {
|
|
7
8
|
return { type: 'object', additionalProperties: false, required, properties };
|
|
@@ -83,9 +84,11 @@ const skill = object({
|
|
|
83
84
|
const serverField = { type: 'string', description: '授权服务地址的 origin,默认生产环境,ZAN_SAAS_OAUTH_BASE_URL 可覆盖;凭证按它分开保存。' };
|
|
84
85
|
const time = { type: ['string', 'null'], description: 'ISO 8601 时间,服务端未返回有效期时为 null。' };
|
|
85
86
|
const credentialStatus = object({
|
|
86
|
-
loggedIn: { type: 'boolean', description: '
|
|
87
|
+
loggedIn: { type: 'boolean', description: '本机个人登录未标记需要重新登录,且已知刷新有效期未到期;未知有效期时保留已有记录。不表示环境token或服务端权限有效。' },
|
|
87
88
|
server: serverField,
|
|
88
|
-
clientId: text, tool: text, loginAt: time, accessExpiresAt: time, refreshExpiresAt: time
|
|
89
|
+
clientId: text, tool: text, loginAt: time, accessExpiresAt: time, refreshExpiresAt: time,
|
|
90
|
+
credentialSource: { enum: ['login', 'env', 'missing'], description: '按当前配置选中的凭证来源:个人登录、环境token或缺失。离线状态不证明服务端有效。' },
|
|
91
|
+
environmentTokenIgnored: { type: 'boolean', description: '已选个人登录且同时存在环境token时为true;环境token不会覆盖该登录。' }
|
|
89
92
|
}, ['loggedIn', 'server']);
|
|
90
93
|
const shopIdField = {
|
|
91
94
|
type: ['string', 'null'], description: '业务命令当前使用的店铺 ID;没有有效上下文时为 null。'
|
|
@@ -108,7 +111,7 @@ const savedShop = object({
|
|
|
108
111
|
context: { $ref: '#/definitions/context' }, shopName: { type: ['string', 'null'] }
|
|
109
112
|
});
|
|
110
113
|
const accountNote = '调用账号接口网关,不需要店铺;默认生产环境,ZAN_SAAS_ACCOUNT_API_BASE_URL 可覆盖,token 规则与业务命令相同。';
|
|
111
|
-
const contextNote = '
|
|
114
|
+
const contextNote = '上下文优先级:显式程序注入、ZAN_SAAS_CONTEXT、宿主会话保存位置;无会话ID时使用本机共享选择。无效显式输入不回退。';
|
|
112
115
|
|
|
113
116
|
const contracts = {
|
|
114
117
|
install: {
|
|
@@ -214,7 +217,7 @@ const contracts = {
|
|
|
214
217
|
'token 写入 ~/.zan-saas/credentials.json(权限 0600),按授权服务地址分开保存;输出不含 token。',
|
|
215
218
|
'用户取消或超时(5 分钟)时失败退出,不自动重试。',
|
|
216
219
|
'登录后用短超时尝试开通有赞云账号,未确认时只记录 ensured: false,不因此要求重新登录。',
|
|
217
|
-
'
|
|
220
|
+
'登录成功后按宿主生命周期初始化或恢复会话,再用 shop current 查看实际位置。账号选店、连锁内切换和临时目标分别读取 shop use、shop unit use、shop resolve 契约。',
|
|
218
221
|
'浏览器授权完成不代表本命令成功;换取凭证失败时未保存新凭证,停止后续选店和业务请求,按错误核查版本、配置及授权服务路由。'
|
|
219
222
|
]
|
|
220
223
|
},
|
|
@@ -232,8 +235,9 @@ const contracts = {
|
|
|
232
235
|
server: serverField,
|
|
233
236
|
localCredential: { enum: ['deleted', 'delete_failed'],
|
|
234
237
|
description: 'deleted 表示当前个人凭证已在本机清除或已不存在;delete_failed 表示本地清理未成功。' },
|
|
235
|
-
remoteRevocation: { enum: ['revoked', 'failed', 'unknown'],
|
|
236
|
-
description: 'revoked
|
|
238
|
+
remoteRevocation: { enum: ['revoked', 'failed', 'unknown', 'not_attempted'],
|
|
239
|
+
description: 'revoked 已确认撤销;failed 接口拒绝;unknown 无法确认结果;not_attempted 表示没有个人凭证可撤销。' },
|
|
240
|
+
localContext: { enum: ['delete_failed'], description: '选店记录清理失败时出现;凭证清理与远端撤销仍已分别尝试。' },
|
|
237
241
|
httpStatus: { type: 'integer', minimum: 100, maximum: 599,
|
|
238
242
|
description: '仅取得撤销接口失败 HTTP 响应时出现,不回显响应正文。' }
|
|
239
243
|
}, ['command', 'server', 'localCredential', 'remoteRevocation'])
|
|
@@ -295,17 +299,17 @@ const contracts = {
|
|
|
295
299
|
]
|
|
296
300
|
},
|
|
297
301
|
'shop.use': {
|
|
298
|
-
input: object({ kdtId: id }),
|
|
299
|
-
output: object({
|
|
302
|
+
input: object({ kdtId: id, sessionOnly: { type: 'boolean', description: '只更新本会话,不更新最近位置;需要会话ID。' } }, ['kdtId']),
|
|
303
|
+
output: { oneOf: [object({
|
|
300
304
|
context: { $ref: '#/definitions/context' }, shopName: { type: ['string', 'null'] },
|
|
301
305
|
overriddenByEnv: { type: 'boolean', description: '为 true 时 ZAN_SAAS_CONTEXT 已设置,业务命令仍以它为准。' }
|
|
302
|
-
}),
|
|
306
|
+
}), shopModels.chosenOutput('use_root')] },
|
|
303
307
|
definitions: { context },
|
|
304
308
|
notes: [
|
|
305
309
|
accountNote,
|
|
306
310
|
'先查询当前用户,再在其店铺列表中确认该店铺,只接受总部或单店;hq 与 shopId 都取该店铺 ID。',
|
|
307
|
-
'
|
|
308
|
-
'
|
|
311
|
+
'当前用户在该店铺的员工账号已停用时拒绝。共享模式员工状态查询失败保留旧兼容行为;会话模式要求核实成功,失败不保存选择。',
|
|
312
|
+
'无会话ID时写入 ~/.zan-saas/context.json;会话模式独立保存并默认更新供新会话继承的位置。上下文不含凭证,不证明权限。',
|
|
309
313
|
'首次选择与切换店铺均使用本命令,无需重新登录;保存后用 shop current 确认实际店铺,overriddenByEnv 为 true 时不能声称当前上下文已切换。',
|
|
310
314
|
contextNote
|
|
311
315
|
]
|
|
@@ -313,19 +317,23 @@ const contracts = {
|
|
|
313
317
|
'shop.current': {
|
|
314
318
|
input: empty,
|
|
315
319
|
output: object({
|
|
316
|
-
source: { enum: ['env', 'saved', 'missing'] },
|
|
320
|
+
source: { enum: ['env', 'saved', 'missing', 'program'] },
|
|
317
321
|
status: { enum: ['ready', 'invalid', 'missing'] },
|
|
318
322
|
context: nullableContext,
|
|
319
323
|
saved: { anyOf: [savedShop, { type: 'null' }] },
|
|
320
|
-
requestShopId: { type: ['string', 'null'], description: '环境变量 ZAN_SAAS_KDT_ID 的有效值;未设置或无效时为 null。不代表所有命令的最终请求店铺。' }
|
|
321
|
-
|
|
324
|
+
requestShopId: { type: ['string', 'null'], description: '环境变量 ZAN_SAAS_KDT_ID 的有效值;未设置或无效时为 null。不代表所有命令的最终请求店铺。' },
|
|
325
|
+
binding: shopModels.binding,
|
|
326
|
+
selection: { anyOf: [shopModels.selection, { type: 'null' }] }
|
|
327
|
+
}, ['source', 'status', 'context', 'saved', 'requestShopId']),
|
|
322
328
|
definitions: { context },
|
|
323
329
|
notes: ['只读本地文件和环境变量,不联网。requestShopId 是环境变量值;部分命令按其目标店铺设置请求头,具体以命令契约为准。', contextNote]
|
|
324
330
|
},
|
|
325
331
|
'shop.clear': {
|
|
326
|
-
input:
|
|
327
|
-
output: object({ cleared: { type: 'boolean', description: '
|
|
328
|
-
|
|
332
|
+
input: object({ recent: { type: 'boolean', description: '同时清除该账号环境的最近位置;仅会话模式支持。' } }, []),
|
|
333
|
+
output: object({ cleared: { type: 'boolean', description: '已清除选中的店铺;重复清除为false。' }, server: serverField,
|
|
334
|
+
binding: shopModels.binding, currentChanged: { type: 'boolean' }, recentChanged: { type: 'boolean' } },
|
|
335
|
+
['cleared', 'server']),
|
|
336
|
+
notes: ['会话模式保留cleared记录,避免重新继承;默认不影响其他会话或最近位置。原文件内改写,不删除或替换上下文文件。']
|
|
329
337
|
},
|
|
330
338
|
'skills.list': {
|
|
331
339
|
input: empty,
|
|
@@ -356,7 +364,7 @@ const contracts = {
|
|
|
356
364
|
}),
|
|
357
365
|
notes: [
|
|
358
366
|
'按明确的 API 名和版本发送 POST,可以是已登记或未登记的接口。风险未知,发送需 --yes;不继承领域命令的对象校验、风险分级和业务结果保证。',
|
|
359
|
-
'默认发送到生产开放 API 网关,ZAN_SAAS_API_BASE_URL
|
|
367
|
+
'默认发送到生产开放 API 网关,ZAN_SAAS_API_BASE_URL 可覆盖。默认优先使用已存个人登录,仅无个人记录时使用环境token;显式 ZAN_SAAS_AUTH_SOURCE=env 可选择环境凭证,不接受任意目标 URL。',
|
|
360
368
|
'退出0、HTTP 2xx 和 sent=true 只表示收到响应;即使正文报告业务失败也不改变退出码,不证明业务成功或写入完成。超时或断连时结果未知,不自动重试。',
|
|
361
369
|
'不提供 --dry-run,不自动应用业务金额或日期转换。'
|
|
362
370
|
]
|
|
@@ -364,7 +372,7 @@ const contracts = {
|
|
|
364
372
|
};
|
|
365
373
|
|
|
366
374
|
function withSystemSchema(command, name) {
|
|
367
|
-
const schema = { schemaVersion: 1, error, ...contracts[name] };
|
|
375
|
+
const schema = { schemaVersion: 1, error, ...(contracts[name] || shopModels.contracts[name]) };
|
|
368
376
|
schema.error = { anyOf: [schema.error, structuredClone(parserErrorModel)] };
|
|
369
377
|
attachCommandSchema(command, schema);
|
|
370
378
|
return command;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const object = (properties, required = Object.keys(properties)) => ({
|
|
4
|
+
type: 'object', additionalProperties: false, required, properties
|
|
5
|
+
});
|
|
6
|
+
const id = { type: 'string', pattern: '^[1-9][0-9]*$', description: '正整数 ID 文本。' };
|
|
7
|
+
const nullableText = { type: ['string', 'null'], description: '已核实的名称或时间;缺失为 null。' };
|
|
8
|
+
const context = object({
|
|
9
|
+
userId: { type: 'string', minLength: 1, description: '已核实的账号 ID,不是客户或员工记录 ID。' },
|
|
10
|
+
hq: { ...id, description: '租户根组织 ID;单店取自身。' },
|
|
11
|
+
shopId: { ...id, description: '当前操作组织 ID,不自动代表全部连锁。' },
|
|
12
|
+
orgType: { enum: ['hq', 'shop'], description: '总部位置或受支持的非总部位置,不是详细组织角色。' }
|
|
13
|
+
});
|
|
14
|
+
const selection = object({
|
|
15
|
+
tenantRootKdtId: id, tenantName: nullableText, currentUnitKdtId: id, unitName: nullableText,
|
|
16
|
+
position: { enum: ['hq', 'shop'], description: '总部或非总部执行位置。' },
|
|
17
|
+
role: { type: ['integer', 'null'], minimum: 0, description: '有来源的组织角色:0单店、1总部、2001门店、2002网店;未知或未核实为null。' }
|
|
18
|
+
});
|
|
19
|
+
const binding = object({
|
|
20
|
+
mode: { enum: ['session', 'shared'], description: '宿主独立会话或本机共享选择。' },
|
|
21
|
+
sessionRef: { type: ['string', 'null'], description: '会话摘要短引用,共享模式为null。' },
|
|
22
|
+
state: { enum: ['uninitialized', 'needs_selection', 'ready', 'cleared', 'identity_pending', 'invalid'],
|
|
23
|
+
description: '分别为尚未初始化、待选店、就绪、已清除、待核实账号、无效。只有就绪的保存绑定提供上下文。' },
|
|
24
|
+
revision: { type: ['integer', 'null'], minimum: 1, maximum: Number.MAX_SAFE_INTEGER,
|
|
25
|
+
description: '绑定版本;无记录为null,冲突不覆盖较新选择。' },
|
|
26
|
+
origin: { enum: ['explicit', 'inherited', 'legacy_import', 'none'],
|
|
27
|
+
description: '明确选择、初始化继承、旧共享记录导入或无位置来源。' },
|
|
28
|
+
initializedAt: nullableText, selectedAt: nullableText
|
|
29
|
+
});
|
|
30
|
+
const nullable = value => ({ anyOf: [value, { type: 'null' }] });
|
|
31
|
+
const effect = action => object({
|
|
32
|
+
action: { const: action },
|
|
33
|
+
currentChanged: { type: 'boolean', description: '本次是否改变当前会话绑定。' },
|
|
34
|
+
recentChanged: { type: 'boolean', description: '本次是否改变供新会话继承的位置。' }
|
|
35
|
+
});
|
|
36
|
+
const chosenOutput = action => object({
|
|
37
|
+
context: nullable(context), selection: nullable(selection), binding, effect: effect(action)
|
|
38
|
+
});
|
|
39
|
+
const diagnosticCode = { anyOf: [
|
|
40
|
+
{ type: 'integer', minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER },
|
|
41
|
+
{ type: 'string', pattern: '^-?\\d{1,12}$' }
|
|
42
|
+
] };
|
|
43
|
+
const trace = { type: 'string', pattern: '^[a-zA-Z0-9_-]{1,128}$' };
|
|
44
|
+
const locationError = command => object({
|
|
45
|
+
success: { const: false }, code: { const: -10001 }, message: { type: 'string' },
|
|
46
|
+
details: object({
|
|
47
|
+
category: { enum: ['validation', 'configuration', 'authentication', 'permission', 'http',
|
|
48
|
+
'transport', 'protocol', 'business_rejected', 'limit', 'scope_unresolved'] },
|
|
49
|
+
stage: { enum: ['scope', 'configuration', 'headquarters-binding', 'target-membership'] },
|
|
50
|
+
command: { const: command },
|
|
51
|
+
targetScope: object({ kdtId: { anyOf: [id,
|
|
52
|
+
{ type: 'integer', minimum: 1, maximum: Number.MAX_SAFE_INTEGER }
|
|
53
|
+
], description: '失败阶段核验的总部或目标店铺,不表示已切换位置。' } }),
|
|
54
|
+
requestState: { enum: ['not_sent', 'sent', 'unknown'] },
|
|
55
|
+
nextStep: { type: 'string', minLength: 1 },
|
|
56
|
+
api: { type: 'string', pattern: '^[a-z][a-z0-9._-]*/\\d+\\.\\d+\\.\\d+$' },
|
|
57
|
+
httpStatus: { type: 'integer', minimum: 100, maximum: 599 },
|
|
58
|
+
businessCode: diagnosticCode, gatewayCode: diagnosticCode,
|
|
59
|
+
trace_id: trace, request_id: trace
|
|
60
|
+
}, ['category', 'stage', 'command', 'targetScope', 'requestState', 'nextStep'])
|
|
61
|
+
});
|
|
62
|
+
const notes = [
|
|
63
|
+
'上下文及本地保存不证明员工权限;账号、组织关系和业务授权分别核验。失败后不自动换店、换身份或重放。',
|
|
64
|
+
'会话模式由宿主逐次注入 ZAN_SAAS_SESSION_ID。已有会话不跟随其他会话,查询和恢复不更新最近位置。'
|
|
65
|
+
];
|
|
66
|
+
const locationNotes = [
|
|
67
|
+
'总部绑定或目录核验失败时可返回受限 details:category、stage、实际 command、targetScope、requestState 和 nextStep;可用时保留 API、HTTP 状态、数字业务码及安全 trace/request 标识,不回显响应正文、凭证或上游自由文本。',
|
|
68
|
+
'targetScope 是失败检查的对象,总部请求失败时可为总部 ID,不代表已选择目标。失败不改变当前位置或最近位置;旧的本地错误及公共参数解析错误仍保留各自模型。'
|
|
69
|
+
];
|
|
70
|
+
const switchInput = {
|
|
71
|
+
kdtId: id, sessionOnly: { type: 'boolean', description: '仅更新本会话,需要宿主会话标识;省略为false。' }
|
|
72
|
+
};
|
|
73
|
+
const optionalInput = properties => ({ ...object(properties), required: [] });
|
|
74
|
+
const contracts = {
|
|
75
|
+
'shop.session.init': {
|
|
76
|
+
input: optionalInput({ resume: { type: 'boolean', description: '恢复原会话,缺记录则失败;省略为false。' } }),
|
|
77
|
+
output: chosenOutput('init'), notes: [...notes,
|
|
78
|
+
'显式初始化可核实当前账号并创建绑定。首次继承一次;无最近位置返回needs_selection;恢复已有记录不改时间和最近位置。']
|
|
79
|
+
},
|
|
80
|
+
'shop.unit.use': {
|
|
81
|
+
error: { anyOf: [object({ success: { const: false }, code: { const: -10001 },
|
|
82
|
+
message: { type: 'string' } }), locationError('shop.unit.use')] },
|
|
83
|
+
input: { ...object(switchInput), required: ['kdtId'] },
|
|
84
|
+
output: chosenOutput('use_unit'), notes: [...notes, ...locationNotes,
|
|
85
|
+
'需要已初始化会话及当前租户。只支持已核实的当前连锁总部、门店或网店,不改变租户根。']
|
|
86
|
+
},
|
|
87
|
+
'shop.resolve': {
|
|
88
|
+
error: { anyOf: [object({ success: { const: false }, code: { const: -10001 },
|
|
89
|
+
message: { type: 'string' } }), locationError('shop.resolve')] },
|
|
90
|
+
input: { ...object({ kdtId: id, rootKdtId: { ...id, description: '目标所属根组织;省略时目标须为账号层根。' } }),
|
|
91
|
+
required: ['kdtId'] },
|
|
92
|
+
output: object({ context, selection, effect: effect('resolve'), revision: { type: 'null' } }),
|
|
93
|
+
notes: [...notes, ...locationNotes, '不保存当前位置。把返回context固定为本次任务每次调用的显式上下文;任务结束后丢弃。']
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
module.exports = { binding, selection, chosenOutput, contracts };
|
|
@@ -10,12 +10,31 @@ const {
|
|
|
10
10
|
const { withSystemSchema } = require('./contracts');
|
|
11
11
|
const { createPrinter, formatOption } = require('./printer');
|
|
12
12
|
const { shopSelectionHint } = require('./shop-guidance');
|
|
13
|
+
const sessions = require('../../identity/session-store');
|
|
14
|
+
const locations = require('../../identity/shop-location');
|
|
15
|
+
const { snapshotApiIdentity } = require('../../transport/api');
|
|
16
|
+
const { withFileLock } = require('../../auth/credential-store');
|
|
13
17
|
|
|
14
18
|
const idPattern = /^[1-9]\d*$/;
|
|
15
|
-
const orgLabels = { hq: '总部', shop: '
|
|
19
|
+
const orgLabels = { hq: '总部', shop: '非总部组织' };
|
|
16
20
|
const roleLabel = role => account.shopRoles[role] || '未知类型';
|
|
17
21
|
const envNote = '环境变量 ZAN_SAAS_CONTEXT 已设置,业务命令仍以它为准。';
|
|
18
22
|
|
|
23
|
+
function sharedResult(value, savedEntry) {
|
|
24
|
+
const context = value.context;
|
|
25
|
+
const same = context && savedEntry && ['userId', 'hq', 'shopId', 'orgType']
|
|
26
|
+
.every(field => context[field] === savedEntry.context[field]);
|
|
27
|
+
return { ...value,
|
|
28
|
+
binding: { mode: 'shared', sessionRef: null, state: savedEntry ? 'ready' : 'needs_selection',
|
|
29
|
+
revision: null, origin: savedEntry ? 'explicit' : 'none',
|
|
30
|
+
initializedAt: null, selectedAt: savedEntry?.savedAt ?? null },
|
|
31
|
+
selection: context ? locations.selection(context, {
|
|
32
|
+
tenantName: same && context.hq === context.shopId ? savedEntry.shopName : null,
|
|
33
|
+
unitName: same ? savedEntry.shopName : null
|
|
34
|
+
}) : null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
function positiveInteger(name, max = Number.MAX_SAFE_INTEGER) {
|
|
20
39
|
return value => {
|
|
21
40
|
if (!idPattern.test(value) || Number(value) > max) {
|
|
@@ -65,10 +84,11 @@ async function checkStaffStatuses(items, env, concurrency = 5) {
|
|
|
65
84
|
// Hides the shops where the user's staff account is disabled, unless --all; total stays the server's count.
|
|
66
85
|
async function listShops(options = {}, env = process.env) {
|
|
67
86
|
const name = typeof options.name === 'string' ? options.name.trim() : '';
|
|
87
|
+
const { environment } = await snapshotApiIdentity(env);
|
|
68
88
|
const page = await account.listShops({
|
|
69
89
|
...(name ? { name } : {}), pageNum: options.pageNo ?? 1, pageSize: options.pageSize ?? 20,
|
|
70
90
|
lineOfBusinesses: options.lineOfBusiness ?? account.defaultLineOfBusinesses
|
|
71
|
-
},
|
|
91
|
+
}, environment);
|
|
72
92
|
// Staff filtering may hide the entire page; only the server's unfiltered total determines continuation.
|
|
73
93
|
const hasMore = page.total === null ? null :
|
|
74
94
|
BigInt(page.pageNum) * BigInt(page.pageSize) < BigInt(page.total);
|
|
@@ -77,7 +97,7 @@ async function listShops(options = {}, env = process.env) {
|
|
|
77
97
|
windowLimitReached: hasMore !== false && page.pageNum === Number.MAX_SAFE_INTEGER });
|
|
78
98
|
if (options.all) return { ...page, totalScope: 'before_staff_filter', hidden: 0,
|
|
79
99
|
hiddenReason: null, nextStep: null, unchecked: [], uncheckedReason: null };
|
|
80
|
-
const checks = await checkStaffStatuses(page.items,
|
|
100
|
+
const checks = await checkStaffStatuses(page.items, environment);
|
|
81
101
|
const items = page.items.filter((_, index) => checks[index].status !== account.staffStatuses.disabled);
|
|
82
102
|
const unchecked = page.items.filter((_, index) => checks[index].error).map(item => item.kdtId);
|
|
83
103
|
const failed = checks.find(check => check.error);
|
|
@@ -92,12 +112,15 @@ async function listShops(options = {}, env = process.env) {
|
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
// Confirms the shop through the user's own shop list before saving it as the context.
|
|
95
|
-
async function useShop(kdtId, env = process.env) {
|
|
115
|
+
async function useShop(kdtId, env = process.env, options = {}, explicitContext) {
|
|
116
|
+
if (sessions.sessionId(env)) return locations.select(kdtId, options, env, explicitContext);
|
|
117
|
+
if (options.sessionOnly) throw sessions.fail('session_missing', '--session-only 需要宿主会话标识。');
|
|
96
118
|
if (typeof kdtId !== 'string' || !idPattern.test(kdtId) || !Number.isSafeInteger(Number(kdtId))) {
|
|
97
119
|
throw new Error('kdtId 必须是正整数店铺 ID。');
|
|
98
120
|
}
|
|
99
|
-
const
|
|
100
|
-
const
|
|
121
|
+
const actor = await locations.identify(env);
|
|
122
|
+
const user = actor.user;
|
|
123
|
+
const page = await account.listShops({ includeKdtIds: [kdtId], pageNum: 1, pageSize: 1 }, actor.pinned);
|
|
101
124
|
const shop = page.items.find(item => item.kdtId === kdtId);
|
|
102
125
|
if (!shop) {
|
|
103
126
|
throw new Error(`当前账号下没有可选店铺 ${kdtId}。执行 zan-saas shop list 查看可选店铺。`);
|
|
@@ -106,28 +129,54 @@ async function useShop(kdtId, env = process.env) {
|
|
|
106
129
|
throw new Error(`店铺 ${kdtId} 是${roleLabel(shop.shopRole)},暂时只能选择总部或单店。`);
|
|
107
130
|
}
|
|
108
131
|
// A failed check does not block: the business gateway still verifies the user against the shop.
|
|
109
|
-
const status = await account.staffStatus(kdtId,
|
|
132
|
+
const status = await account.staffStatus(kdtId, actor.pinned).catch(() => null);
|
|
110
133
|
if (status === account.staffStatuses.disabled) {
|
|
111
134
|
throw new Error(`你在店铺 ${kdtId} 的员工账号已停用,不能选择。`);
|
|
112
135
|
}
|
|
113
|
-
const context =
|
|
114
|
-
|
|
136
|
+
const context = await withFileLock(env, async () => {
|
|
137
|
+
locations.assertIdentity(actor, env);
|
|
138
|
+
return saveContext({ userId: user.userId, hq: kdtId, shopId: kdtId, orgType: shop.orgType },
|
|
139
|
+
{ shopName: shop.shopName }, env);
|
|
140
|
+
});
|
|
115
141
|
return { context, shopName: shop.shopName, overriddenByEnv: Boolean(env.ZAN_SAAS_CONTEXT) };
|
|
116
142
|
}
|
|
117
143
|
|
|
118
|
-
function currentShop(env = process.env) {
|
|
144
|
+
function currentShop(env = process.env, explicitContext) {
|
|
145
|
+
if (sessions.sessionId(env)) {
|
|
146
|
+
const snapshot = sessions.snapshot(env);
|
|
147
|
+
const location = snapshot.binding?.location;
|
|
148
|
+
const saved = location ? { context: location.context, shopName: locations.displayName(location) } : null;
|
|
149
|
+
const source = explicitContext !== undefined ? 'program' :
|
|
150
|
+
env.ZAN_SAAS_CONTEXT ? 'env' : saved ? 'saved' : 'missing';
|
|
151
|
+
const state = readContext(source === 'program' ? explicitContext :
|
|
152
|
+
source === 'env' ? env.ZAN_SAAS_CONTEXT : location?.context);
|
|
153
|
+
const context = state.context ?? null;
|
|
154
|
+
return { source, status: state.status, context, saved, requestShopId: requestShopIdOf(env),
|
|
155
|
+
binding: sessions.publicBinding(snapshot),
|
|
156
|
+
selection: context ? (location && ['userId', 'hq', 'shopId', 'orgType']
|
|
157
|
+
.every(field => context[field] === location.context[field])
|
|
158
|
+
? location.selection : locations.selection(context)) : null };
|
|
159
|
+
}
|
|
119
160
|
const savedEntry = readSavedContext(env);
|
|
120
161
|
const saved = savedEntry ? { context: savedEntry.context, shopName: savedEntry.shopName } : null;
|
|
121
162
|
const requestShopId = requestShopIdOf(env);
|
|
122
|
-
if (env.ZAN_SAAS_CONTEXT) {
|
|
123
|
-
const state = readContext(env.ZAN_SAAS_CONTEXT);
|
|
124
|
-
return { source:
|
|
163
|
+
if (explicitContext !== undefined || env.ZAN_SAAS_CONTEXT) {
|
|
164
|
+
const state = readContext(explicitContext !== undefined ? explicitContext : env.ZAN_SAAS_CONTEXT);
|
|
165
|
+
return sharedResult({ source: explicitContext !== undefined ? 'program' : 'env',
|
|
166
|
+
status: state.status, context: state.context ?? null, saved, requestShopId }, savedEntry);
|
|
125
167
|
}
|
|
126
|
-
return saved ? { source: 'saved', status: 'ready', context: saved.context, saved, requestShopId } :
|
|
127
|
-
{ source: 'missing', status: 'missing', context: null, saved: null, requestShopId };
|
|
168
|
+
return sharedResult(saved ? { source: 'saved', status: 'ready', context: saved.context, saved, requestShopId } :
|
|
169
|
+
{ source: 'missing', status: 'missing', context: null, saved: null, requestShopId }, savedEntry);
|
|
128
170
|
}
|
|
129
171
|
|
|
130
|
-
function clearShop(env = process.env) {
|
|
172
|
+
function clearShop(env = process.env, options = {}, explicitContext) {
|
|
173
|
+
if (sessions.sessionId(env)) {
|
|
174
|
+
locations.rejectOverride(env, explicitContext);
|
|
175
|
+
const effect = sessions.clear(options, env);
|
|
176
|
+
return { ...effect, server: oauthConfig(env).server,
|
|
177
|
+
binding: sessions.publicBinding(sessions.snapshot(env)) };
|
|
178
|
+
}
|
|
179
|
+
if (options.recent) throw sessions.fail('session_missing', '--recent 需要宿主会话标识。');
|
|
131
180
|
return { cleared: deleteContext(env), server: oauthConfig(env).server };
|
|
132
181
|
}
|
|
133
182
|
|
|
@@ -154,42 +203,71 @@ const text = {
|
|
|
154
203
|
if (value.items.some(item => item.orgType)) lines.push(shopSelectionHint);
|
|
155
204
|
return lines.join('\n');
|
|
156
205
|
},
|
|
157
|
-
use: value =>
|
|
206
|
+
use: value => value.effect
|
|
207
|
+
? `已切换到店铺 ${locations.displayName(value) || value.context.shopId}。\n` +
|
|
208
|
+
`${value.effect.recentChanged ? '已更新供新会话继承的位置。' : '其他会话保持各自位置。'}\n确认当前店铺:zan-saas shop current`
|
|
209
|
+
: `${value.overriddenByEnv ? '已保存店铺' : '已切换到店铺'} ${describe(value.context, value.shopName)}。` +
|
|
158
210
|
(value.overriddenByEnv ? `\n${envNote}` : '') +
|
|
159
211
|
'\n确认当前店铺:zan-saas shop current',
|
|
160
212
|
current: value => {
|
|
213
|
+
if (value.binding?.mode === 'session') {
|
|
214
|
+
const name = value.selection && locations.displayName({ selection: value.selection });
|
|
215
|
+
const states = { uninitialized: '尚未初始化', identity_pending: '当前账号尚未核实',
|
|
216
|
+
needs_selection: '尚未选择店铺', cleared: '已清除选择', invalid: '记录无效', ready: '就绪' };
|
|
217
|
+
return `${value.context ? `当前店铺:${name || value.context.shopId}` : '当前没有有效店铺。'}\n` +
|
|
218
|
+
`会话状态:${states[value.binding.state]};位置来源:${value.source}。` +
|
|
219
|
+
(value.source === 'env' || value.source === 'program' ? '\n显式上下文覆盖保存的位置。' : '') +
|
|
220
|
+
(value.requestShopId ? `\n请求店铺头覆盖:${value.requestShopId},具体目标须按命令核验。` : '');
|
|
221
|
+
}
|
|
161
222
|
const requestNote = value.requestShopId
|
|
162
223
|
? `\n环境变量 ZAN_SAAS_KDT_ID 设置为 ${value.requestShopId};具体请求店铺以所用命令和目标参数为准。`
|
|
163
224
|
: '';
|
|
164
|
-
if (value.source === 'env') {
|
|
225
|
+
if (value.source === 'env' || value.source === 'program') {
|
|
165
226
|
const head = value.status === 'ready'
|
|
166
|
-
?
|
|
227
|
+
? `当前上下文来自${value.source === 'env' ? '环境变量 ZAN_SAAS_CONTEXT' : '程序注入'}(kdtId ${value.context.shopId},${orgLabels[value.context.orgType]})。`
|
|
167
228
|
: '环境变量 ZAN_SAAS_CONTEXT 无效,业务命令会拒绝执行。';
|
|
168
229
|
return `${value.saved ? `${head}\n已保存的店铺 ${describe(value.saved.context, value.saved.shopName)} 被它覆盖。` : head}${requestNote}`;
|
|
169
230
|
}
|
|
170
231
|
if (value.source === 'saved') {
|
|
171
|
-
return `当前店铺:${describe(value.context, value.saved.shopName)}${requestNote}\n${shopSelectionHint}
|
|
232
|
+
return `当前店铺:${describe(value.context, value.saved.shopName)}${requestNote}\n${shopSelectionHint}\n当前使用本机共享选店,其他会话切店可能影响后续调用。`;
|
|
172
233
|
}
|
|
173
234
|
return `未选择店铺。${requestNote}\n${shopSelectionHint}`;
|
|
174
235
|
},
|
|
175
|
-
clear: value => (value.cleared ? '已清除保存的店铺。' : '没有保存的店铺。')
|
|
236
|
+
clear: value => (value.cleared ? '已清除保存的店铺。' : '没有保存的店铺。') +
|
|
237
|
+
(value.binding ? '\n本会话保留清除状态,不会自动继承最近位置。' : ''),
|
|
238
|
+
init: value => value.context
|
|
239
|
+
? `本会话当前店铺:${locations.displayName(value) || value.context.shopId}。`
|
|
240
|
+
: '本会话已初始化,请明确选择店铺。',
|
|
241
|
+
resolve: value => `本次目标:${locations.displayName(value) || value.context.shopId}。\n当前位置和最近位置均未改变。`
|
|
176
242
|
};
|
|
177
243
|
const printer = createPrinter(text);
|
|
178
244
|
|
|
179
245
|
const defaults = {
|
|
180
|
-
list: options => listShops(options), use: kdtId => useShop(kdtId
|
|
181
|
-
|
|
246
|
+
list: options => listShops(options), use: (kdtId, options) => useShop(kdtId, process.env, options),
|
|
247
|
+
current: () => currentShop(), clear: options => clearShop(process.env, options),
|
|
248
|
+
init: options => locations.initialize(options),
|
|
249
|
+
unit: (kdtId, options) => locations.select(kdtId, { ...options, unit: true }),
|
|
250
|
+
resolve: (kdtId, options) => locations.resolve(kdtId, options),
|
|
251
|
+
currentShopId: () => shopIdOf(process.env)
|
|
182
252
|
};
|
|
183
253
|
|
|
184
|
-
function createShopCommand(impl =
|
|
254
|
+
function createShopCommand(impl, { context } = {}) {
|
|
255
|
+
impl ||= { ...defaults,
|
|
256
|
+
use: (kdtId, options) => useShop(kdtId, process.env, options, context),
|
|
257
|
+
unit: (kdtId, options) => locations.select(kdtId, { ...options, unit: true }, process.env, context),
|
|
258
|
+
current: () => currentShop(process.env, context),
|
|
259
|
+
clear: options => clearShop(process.env, options, context) };
|
|
185
260
|
const shop = new Command('shop').description('查看可用店铺并设置当前店铺')
|
|
186
261
|
.addHelpText('after', `
|
|
187
|
-
|
|
262
|
+
登录成功后查看当前位置,再按本次意图选路,无需重新登录:
|
|
263
|
+
会话首次使用:zan-saas shop session init;恢复时加 --resume
|
|
188
264
|
1. 查看可选店铺:zan-saas shop list
|
|
189
265
|
2. 用户选定目标后:zan-saas shop use <kdtId>
|
|
190
266
|
3. 确认当前店铺:zan-saas shop current
|
|
191
267
|
将 <kdtId> 替换为目标店铺 ID;店铺较多时可用 shop list --name <keyword> 查找。
|
|
192
268
|
浏览器中切换店铺不会改变 CLI 保存的选择。
|
|
269
|
+
连锁内切换组织:zan-saas shop unit use <kdtId>
|
|
270
|
+
临时查询其他店铺:zan-saas shop resolve <kdtId>,使用返回的 context 执行本次任务。
|
|
193
271
|
ZAN_SAAS_CONTEXT 会覆盖保存的店铺,切换后以 shop current 显示的实际来源为准。
|
|
194
272
|
`);
|
|
195
273
|
withSystemSchema(shop.command('list')
|
|
@@ -216,21 +294,44 @@ windowLimitReached 为 true 表示达到 CLI 可表示的页码上限,请缩
|
|
|
216
294
|
withSystemSchema(shop.command('use')
|
|
217
295
|
.description('选择当前用户的店铺并保存为上下文')
|
|
218
296
|
.argument('<kdtId>', 'shop list 返回的总部或单店 kdtId')
|
|
297
|
+
.option('--session-only', '只改变本会话,不更新新会话继承的位置')
|
|
219
298
|
.addOption(formatOption())
|
|
220
299
|
.addHelpText('after', `
|
|
221
300
|
先确认店铺属于当前账号,再把 userId、hq、shopId、orgType 保存到本地;业务命令随后用它选择店铺。
|
|
222
301
|
已有有效上下文时沿用当前店铺;用户按店名明确选择且唯一匹配,或确认候选后,使用对应 kdtId 执行;本地上下文不是业务接口授权证明。
|
|
223
302
|
环境变量 ZAN_SAAS_CONTEXT 设置时优先于保存的店铺。
|
|
224
303
|
`)
|
|
225
|
-
.action(async (kdtId, options) => { printer('use', options.format)(await impl.use(kdtId)); }), 'shop.use');
|
|
304
|
+
.action(async (kdtId, options) => { printer('use', options.format)(await impl.use(kdtId, options)); }), 'shop.use');
|
|
226
305
|
withSystemSchema(shop.command('current')
|
|
227
306
|
.description('查看业务命令使用的店铺上下文,不访问网络')
|
|
228
307
|
.addOption(formatOption())
|
|
229
308
|
.action(options => { printer('current', options.format)(impl.current()); }), 'shop.current');
|
|
230
309
|
withSystemSchema(shop.command('clear')
|
|
231
310
|
.description('清除本机保存的店铺上下文,不访问网络')
|
|
311
|
+
.option('--recent', '同时清除供新会话继承的位置;需要会话模式')
|
|
312
|
+
.addOption(formatOption())
|
|
313
|
+
.action(options => { printer('clear', options.format)(impl.clear(options)); }), 'shop.clear');
|
|
314
|
+
const session = shop.command('session').description('初始化或恢复宿主会话的选店记录');
|
|
315
|
+
withSystemSchema(session.command('init')
|
|
316
|
+
.description('首次继承最近位置;已有会话保持自己的选择')
|
|
317
|
+
.option('--resume', '恢复原会话;记录缺失时失败,不重新继承')
|
|
318
|
+
.addOption(formatOption())
|
|
319
|
+
.action(async options => printer('init', options.format)(await impl.init(options))), 'shop.session.init');
|
|
320
|
+
const unit = shop.command('unit').description('切换当前连锁内的操作组织');
|
|
321
|
+
withSystemSchema(unit.command('use')
|
|
322
|
+
.description('保留当前连锁根,切换到已核实的门店、网店或总部')
|
|
323
|
+
.argument('<kdtId>', '当前连锁内目标组织的正整数 ID')
|
|
324
|
+
.option('--session-only', '只改变本会话,不更新新会话继承的位置')
|
|
325
|
+
.addOption(formatOption())
|
|
326
|
+
.action(async (kdtId, options) => printer('use', options.format)(await impl.unit(kdtId, options))),
|
|
327
|
+
'shop.unit.use');
|
|
328
|
+
withSystemSchema(shop.command('resolve')
|
|
329
|
+
.description('核实本次临时目标并返回上下文,不改变保存的位置')
|
|
330
|
+
.argument('<kdtId>', '目标店铺根或组织的正整数 ID')
|
|
331
|
+
.option('--root-kdt-id <id>', '目标所属连锁根 ID;省略时目标须为账号层根')
|
|
232
332
|
.addOption(formatOption())
|
|
233
|
-
.action(options =>
|
|
333
|
+
.action(async (kdtId, options) => printer('resolve', options.format)(await impl.resolve(kdtId, options))),
|
|
334
|
+
'shop.resolve');
|
|
234
335
|
return shop;
|
|
235
336
|
}
|
|
236
337
|
|