@youzan-cloud/zan-saas-cli 0.1.0-beta.7 → 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.
Files changed (46) hide show
  1. package/internal/registry/manifest.json +21 -17
  2. package/npm-shrinkwrap.json +2 -2
  3. package/package.json +1 -1
  4. package/skills/zan-saas-order/SKILL.md +12 -1
  5. package/skills/zan-saas-order/references/branch-counts.md +45 -0
  6. package/skills/zan-saas-order/references/order-get.md +2 -0
  7. package/skills/zan-saas-order/references/order-list.md +2 -0
  8. package/skills/zan-saas-order/references/order-scan.md +2 -0
  9. package/skills/zan-saas-org/SKILL.md +2 -0
  10. package/skills/zan-saas-org/references/unit-list.md +2 -0
  11. package/skills/zan-saas-shared/SKILL.md +4 -0
  12. package/skills/zan-saas-shared/references/gateway-errors.md +29 -0
  13. package/skills/zan-saas-shared/references/shop-context.md +3 -1
  14. package/skills/zan-saas-valuecard/SKILL.md +8 -2
  15. package/skills/zan-saas-valuecard/references/transaction-list.md +25 -0
  16. package/skills/zan-saas-valuecard/references/transaction-scan.md +23 -0
  17. package/src/cli/context-help.js +3 -1
  18. package/src/commands/native/access/commands.index.json +1 -1
  19. package/src/commands/native/access/role/list/command.js +4 -32
  20. package/src/commands/native/order/commands.index.json +3 -3
  21. package/src/commands/native/order/order/get/command.js +14 -9
  22. package/src/commands/native/order/order/get/schema.json +197 -41
  23. package/src/commands/native/order/order/list/command.js +9 -6
  24. package/src/commands/native/order/order/list/schema.json +140 -10
  25. package/src/commands/native/order/order/scan/command.js +8 -5
  26. package/src/commands/native/order/order/scan/scan.js +1 -1
  27. package/src/commands/native/order/order/scan/schema.json +135 -8
  28. package/src/commands/native/order/shared/query.js +57 -9
  29. package/src/commands/native/order/shared/search.js +1 -1
  30. package/src/commands/native/org/commands.index.json +4 -4
  31. package/src/commands/native/valuecard/commands.index.json +20 -2
  32. package/src/commands/native/valuecard/domain.json +1 -1
  33. package/src/commands/native/valuecard/transaction/list/command.js +110 -0
  34. package/src/commands/native/valuecard/transaction/list/projection.js +71 -0
  35. package/src/commands/native/valuecard/transaction/list/schema.json +888 -0
  36. package/src/commands/native/valuecard/transaction/scan/command.js +130 -0
  37. package/src/commands/native/valuecard/transaction/scan/projection.js +58 -0
  38. package/src/commands/native/valuecard/transaction/scan/scan.js +70 -0
  39. package/src/commands/native/valuecard/transaction/scan/schema.json +954 -0
  40. package/src/commands/system/shop-contracts.js +35 -4
  41. package/src/execution/error-details.js +13 -18
  42. package/src/execution/gateway-guidance.js +43 -0
  43. package/src/execution/shop-query.js +6 -3
  44. package/src/identity/shop-location.js +18 -13
  45. package/src/identity/unit-membership.js +42 -0
  46. package/src/transport/api.js +1 -1
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const object = properties => ({
4
- type: 'object', additionalProperties: false, required: Object.keys(properties), properties
3
+ const object = (properties, required = Object.keys(properties)) => ({
4
+ type: 'object', additionalProperties: false, required, properties
5
5
  });
6
6
  const id = { type: 'string', pattern: '^[1-9][0-9]*$', description: '正整数 ID 文本。' };
7
7
  const nullableText = { type: ['string', 'null'], description: '已核实的名称或时间;缺失为 null。' };
@@ -36,10 +36,37 @@ const effect = action => object({
36
36
  const chosenOutput = action => object({
37
37
  context: nullable(context), selection: nullable(selection), binding, effect: effect(action)
38
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
+ });
39
62
  const notes = [
40
63
  '上下文及本地保存不证明员工权限;账号、组织关系和业务授权分别核验。失败后不自动换店、换身份或重放。',
41
64
  '会话模式由宿主逐次注入 ZAN_SAAS_SESSION_ID。已有会话不跟随其他会话,查询和恢复不更新最近位置。'
42
65
  ];
66
+ const locationNotes = [
67
+ '总部绑定或目录核验失败时可返回受限 details:category、stage、实际 command、targetScope、requestState 和 nextStep;可用时保留 API、HTTP 状态、数字业务码及安全 trace/request 标识,不回显响应正文、凭证或上游自由文本。',
68
+ 'targetScope 是失败检查的对象,总部请求失败时可为总部 ID,不代表已选择目标。失败不改变当前位置或最近位置;旧的本地错误及公共参数解析错误仍保留各自模型。'
69
+ ];
43
70
  const switchInput = {
44
71
  kdtId: id, sessionOnly: { type: 'boolean', description: '仅更新本会话,需要宿主会话标识;省略为false。' }
45
72
  };
@@ -51,15 +78,19 @@ const contracts = {
51
78
  '显式初始化可核实当前账号并创建绑定。首次继承一次;无最近位置返回needs_selection;恢复已有记录不改时间和最近位置。']
52
79
  },
53
80
  'shop.unit.use': {
81
+ error: { anyOf: [object({ success: { const: false }, code: { const: -10001 },
82
+ message: { type: 'string' } }), locationError('shop.unit.use')] },
54
83
  input: { ...object(switchInput), required: ['kdtId'] },
55
- output: chosenOutput('use_unit'), notes: [...notes,
84
+ output: chosenOutput('use_unit'), notes: [...notes, ...locationNotes,
56
85
  '需要已初始化会话及当前租户。只支持已核实的当前连锁总部、门店或网店,不改变租户根。']
57
86
  },
58
87
  'shop.resolve': {
88
+ error: { anyOf: [object({ success: { const: false }, code: { const: -10001 },
89
+ message: { type: 'string' } }), locationError('shop.resolve')] },
59
90
  input: { ...object({ kdtId: id, rootKdtId: { ...id, description: '目标所属根组织;省略时目标须为账号层根。' } }),
60
91
  required: ['kdtId'] },
61
92
  output: object({ context, selection, effect: effect('resolve'), revision: { type: 'null' } }),
62
- notes: [...notes, '不保存当前位置。把返回context固定为本次任务每次调用的显式上下文;任务结束后丢弃。']
93
+ notes: [...notes, ...locationNotes, '不保存当前位置。把返回context固定为本次任务每次调用的显式上下文;任务结束后丢弃。']
63
94
  }
64
95
  };
65
96
 
@@ -3,6 +3,7 @@
3
3
  const { executionFor } = require('./request-state');
4
4
  const { issueReasons } = require('./error-model');
5
5
  const { ParserError } = require('./parser-error');
6
+ const { gatewayGuidance } = require('./gateway-guidance');
6
7
  const contexts = new WeakMap();
7
8
  const reasons = new Set(issueReasons);
8
9
  const constraintKeys = ['type', 'enum', 'const', 'minimum', 'maximum', 'exclusiveMinimum',
@@ -203,7 +204,8 @@ function localContextReason(error, details, execution) {
203
204
  execution.observedAttempts === 0 ? error.diagnosticReason : undefined;
204
205
  }
205
206
 
206
- function describeRecovery(details, execution, missingAuthentication = false, contextReason, responseReason) {
207
+ function describeRecovery(details, execution, missingAuthentication = false, contextReason, responseReason,
208
+ gateway = gatewayGuidance(details, undefined, execution)) {
207
209
  if (contextReason) return { action: 'stop_and_inspect', nextStep:
208
210
  ['context_missing', 'target_missing'].includes(contextReason)
209
211
  ? '未登录先执行 zan-saas auth login;登录后用 zan-saas shop list 和 zan-saas shop use <kdtId> 选店。缺少上下文时仅可显式指定店铺做 --dry-run,不自动切店或重放。'
@@ -221,14 +223,13 @@ function describeRecovery(details, execution, missingAuthentication = false, con
221
223
  if (details.category === 'provider_unavailable' && details.command === 'privacy.field.decrypt') {
222
224
  return { action: 'stop_and_inspect', nextStep: '当前仅支持离线预览,请加 --dry-run;真实解密服务尚未接入。' };
223
225
  }
226
+ if (responseReason) return { action: 'stop_and_inspect',
227
+ nextStep: `${responseReason.nextStep} 不自动重放、切店或换身份。` };
228
+ if (gateway) return { action: gateway.action, nextStep: gateway.nextStep };
224
229
  if (missingAuthentication || details.category === 'authentication') {
225
230
  return { action: 'complete_authentication',
226
231
  nextStep: '检查 zan-saas auth status --format json,按公共认证流程完成登录;不在聊天中提供凭证,不自动重放原调用。' };
227
232
  }
228
- if (gatewayAccessDenied(details)) {
229
- return { action: 'request_access_review',
230
- nextStep: '请检查当前账号是否有该店铺权限,以及店铺应用是否已获该接口的开放能力授权;可提供接口和 trace_id 给管理员排查,不自动重放。' };
231
- }
232
233
  if (details.category === 'permission') {
233
234
  return { action: 'request_access_review',
234
235
  nextStep: '核对原账号、目标店铺与服务端授权,可提供脱敏接口及 trace_id 给管理员;不切店、换身份或自动重放。' };
@@ -237,8 +238,6 @@ function describeRecovery(details, execution, missingAuthentication = false, con
237
238
  return { action: 'stop_and_inspect',
238
239
  nextStep: '店铺核验失败,后续业务请求尚未发送;本次已有核验请求记录,核对原店铺和凭证后停止,不自动切店或重放。' };
239
240
  }
240
- if (responseReason) return { action: 'stop_and_inspect',
241
- nextStep: `${responseReason.nextStep} 不自动重放、切店或换身份。` };
242
241
  if (details.requestState === 'sent' && details.category === 'business_rejected') {
243
242
  return { action: 'stop_and_inspect',
244
243
  nextStep: '接口已拒绝本次查询;将诊断中的 api、businessCode 或 gatewayCode、trace_id 提供给管理员核查服务端原因。错误码本身不能证明是权限或数据问题,不自动重放、切店或换身份。' };
@@ -275,19 +274,14 @@ function businessRejectionMessage(details) {
275
274
  return `接口拒绝了本次查询${code ? `(${code})` : ''},已停止处理。`;
276
275
  }
277
276
 
278
- function gatewayAccessDenied(details) {
279
- return details?.requestState === 'sent' &&
280
- ['business_rejected', 'permission'].includes(details.category) &&
281
- [4204, '4204'].includes(details.gatewayCode);
282
- }
283
-
284
277
  function finalizeError(error, registeredContext) {
285
278
  if (error instanceof ParserError) return structuredClone(error.result);
286
279
  const context = contexts.get(error) || registeredContext;
287
280
  if (context?.schema.errors?.diagnosticsVersion !== 2) {
288
- if (gatewayAccessDenied(error.cliDetails)) {
289
- error.message = '网关拒绝访问:没有操作权限。';
290
- error.cliDetails.nextStep = '请检查当前账号是否有该店铺权限,以及店铺应用是否已获该接口的开放能力授权;可提供接口和 trace_id 给管理员排查。';
281
+ const gateway = gatewayGuidance(error.cliDetails, context);
282
+ if (gateway) {
283
+ error.message = gateway.message;
284
+ error.cliDetails.nextStep = gateway.nextStep;
291
285
  }
292
286
  return { success: false, code: -10001, message: error.message,
293
287
  ...(error.cliDetails ? { details: error.cliDetails } : {}) };
@@ -372,7 +366,8 @@ function diagnosticError(error, context, execution) {
372
366
  if (error.issuesTruncated) details.issuesTruncated = true;
373
367
  const contextReason = localContextReason(error, details, execution);
374
368
  const responseReason = declaredResponseReason(error, context, details, execution);
375
- const recovery = describeRecovery(details, execution, error.diagnosticReason === 'missing_authentication', contextReason, responseReason);
369
+ const gateway = !inputError && !contextReason && !responseReason ? gatewayGuidance(details, context, execution) : undefined;
370
+ const recovery = describeRecovery(details, execution, error.diagnosticReason === 'missing_authentication', contextReason, responseReason, gateway);
376
371
  const groupConflict = inputError && context.path === 'item.group.list' &&
377
372
  issues.some(issue => issue.path === '/parentGroupId' && issue.reason === 'constraint' && issue.relatedPaths?.includes('/title'))
378
373
  ? context.schema.errors.parentTitleConflict : undefined;
@@ -382,9 +377,9 @@ function diagnosticError(error, context, execution) {
382
377
  const result = { success: false, code: -10001,
383
378
  message: inputError ? (groupConflict?.message || issueMessage(issues)) :
384
379
  contextReason ? localContextMessages[contextReason] :
385
- gatewayAccessDenied(details) ? '网关拒绝访问:没有操作权限。' :
386
380
  details.category === 'provider_unavailable' && context.path === 'privacy.field.decrypt' ? '当前仅支持离线预览,真实解密服务尚未接入' :
387
381
  responseReason ? responseReason.message :
382
+ gateway ? gateway.message :
388
383
  details.category === 'business_rejected' && details.requestState === 'sent' ? businessRejectionMessage(details) :
389
384
  issues.some(issue => issue.source === 'output') ?
390
385
  `返回数据字段 ${issues.find(issue => issue.source === 'output').path} 不符合要求,已停止查询` :
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const authenticationStep = '先区分当前凭证来源与目标店铺、应用授权范围;个人登录使用 zan-saas auth login 重新登录授权,显式环境身份由凭证提供方更新或补齐配置。zan-saas auth status 仅查看本地保存状态,不能证明环境 token 有效;不在聊天中提供凭证,不自动切换来源或刷新,不自动重放。';
4
+ const supportStep = '先按同版 help/schema 核对参数契约;参数符合且持续失败时,将诊断中实际保留的 api、gatewayCode 和追踪号提供给支持人员排查,不提供凭证、客户标识或请求 body,不自动重放。';
5
+ const guidance = new Map([
6
+ [4101, { message: '网关限制了本次请求:调用过于频繁或受限。', action: 'stop_and_inspect',
7
+ nextStep: '停止本轮后续请求,检查调用频率、并发与可用配额;降低频率后由调用方重新决定是否发起。持续受限时提供诊断中实际保留的 api、gatewayCode 和追踪号排查,不保证固定恢复时间,不自动等待或重试,不自动重放。' }],
8
+ [4201, { message: '网关拒绝了本次请求:请求凭证无效。', action: 'complete_authentication',
9
+ nextStep: `核查凭证值以及店铺与应用授权;不能仅凭此码确定密码、员工权限或某一项授权失效。${authenticationStep}` }],
10
+ [4202, { message: '网关拒绝了本次请求:请求凭证已过期。', action: 'complete_authentication',
11
+ nextStep: authenticationStep }],
12
+ [4203, { message: '网关拒绝了本次请求:请求凭证不存在。', action: 'complete_authentication',
13
+ nextStep: authenticationStep }],
14
+ [4204, { message: '网关拒绝访问:没有操作权限。', action: 'request_access_review',
15
+ nextStep: '请检查当前账号是否有该店铺权限,以及店铺应用是否已获该接口的开放能力授权;不能仅凭此码确定根因,持续拒绝时提供诊断中实际保留的 api、gatewayCode 和追踪号给管理员核查服务端限制,不自动申请授权、切店或换身份,不自动重放。' }],
16
+ [4009, { message: '网关无法解析本次请求的客户标识 open_id。', action: 'stop_and_inspect',
17
+ nextStep: '按同版契约核对客户标识、标识类型与目标店铺;这不是登录凭证问题,也不能据此认定客户在所有店铺均不存在,不自动登录或切店,不自动重放。' }],
18
+ [5001, { message: '本次请求失败:网关报告上游系统异常。', action: 'stop_and_inspect', nextStep: supportStep }],
19
+ [5002, { message: '本次请求失败:网关报告上游业务异常。', action: 'stop_and_inspect', nextStep: supportStep }]
20
+ ]);
21
+
22
+ function gatewayGuidance(details, context, execution) {
23
+ if (details?.requestState !== 'sent' ||
24
+ (execution && (execution.requestState !== 'sent' || execution.coverage !== 'complete')) ||
25
+ details.issues?.length) return undefined;
26
+ const raw = details.gatewayCode;
27
+ if (typeof raw !== 'number' && !(typeof raw === 'string' && /^[1-9]\d{3}$/.test(raw))) return undefined;
28
+ const code = Number(raw);
29
+ const selected = guidance.get(code);
30
+ if (!selected) return undefined;
31
+ const compatible = details.category === 'business_rejected' ||
32
+ (details.category === 'authentication' && [4201, 4202, 4203].includes(code)) ||
33
+ (details.category === 'permission' && code === 4204) ||
34
+ (details.category === 'http' && ((details.httpStatus === 429 && code === 4101) ||
35
+ (details.httpStatus >= 500 && [5001, 5002].includes(code))));
36
+ if (!compatible) return undefined;
37
+ const customerOption = code === 4009 && context?.command?.options.some(option => option.long === '--yz-open-id');
38
+ const bindingStep = details.stage === 'shop-binding' ? '店铺核验失败,后续业务请求尚未发送。' : '';
39
+ const customerStep = customerOption ? '核对本命令 --yz-open-id 的客户标识。' : '';
40
+ return { ...selected, nextStep: `${bindingStep}${customerStep}${selected.nextStep}` };
41
+ }
42
+
43
+ module.exports = { gatewayGuidance };
@@ -12,20 +12,23 @@ function failure(command, category, stage, scope = {}, requestState = 'not_sent'
12
12
  return new QueryError(category, stage, scope, facts, requestState, command, nextStep);
13
13
  }
14
14
 
15
- async function createBoundShopQuery(target, command, bindingStage = 'shop-binding', { onResponse, transportOptions } = {}) {
15
+ async function createBoundShopQuery(target, command, bindingStage = 'shop-binding', {
16
+ onResponse, transportOptions, environment, beforeRequest
17
+ } = {}) {
16
18
  const scope = Object.freeze({ kdtId: target.kdtId });
17
19
  if (!Number.isSafeInteger(scope.kdtId) || scope.kdtId < 1) {
18
20
  throw failure(command, 'validation', 'scope');
19
21
  }
20
22
  let env;
21
23
  try {
22
- env = await snapshotApiEnvironment(undefined, scope.kdtId);
24
+ env = await snapshotApiEnvironment(environment, scope.kdtId);
23
25
  } catch (error) {
24
26
  const rejected = failure(command, 'configuration', 'configuration', scope);
25
27
  rejected.diagnosticReason = error.diagnosticReason;
26
28
  throw rejected;
27
29
  }
28
30
  async function read(path, body, stage, successCodes = [200]) {
31
+ if (beforeRequest) beforeRequest(path, stage);
29
32
  const { result, facts, rawBody, responseBytes } = await readApiEnvelope(path, body,
30
33
  (category, details, state, at) => failure(command, category,
31
34
  at === 'configuration' || category === transportOptions?.limitCategory ? at : stage,
@@ -43,7 +46,7 @@ async function createBoundShopQuery(target, command, bindingStage = 'shop-bindin
43
46
  if (data.sid !== scope.kdtId) {
44
47
  throw failure(command, 'scope_unresolved', bindingStage, scope, 'sent', facts);
45
48
  }
46
- return { read, binding };
49
+ return { read, binding, environment: env };
47
50
  }
48
51
 
49
52
  async function createShopQuery(target, command, options = {}) {
@@ -4,7 +4,9 @@ const account = require('../transport/account-api');
4
4
  const { oauthConfig } = require('../auth/config');
5
5
  const { readCredential } = require('../auth/credential-store');
6
6
  const { authSource } = require('../auth/credential-source');
7
- const { requestApi, snapshotApiIdentity, snapshotApiEnvironment } = require('../transport/api');
7
+ const { snapshotApiIdentity } = require('../transport/api');
8
+ const { createBoundShopQuery, failure } = require('../execution/shop-query');
9
+ const { verifyMembership } = require('./unit-membership');
8
10
  const sessions = require('./session-store');
9
11
  const { readSavedContext } = require('./context-store');
10
12
  const { createContext } = require('./context');
@@ -61,22 +63,25 @@ async function rootLocation(kdtId, actor) {
61
63
  }) };
62
64
  }
63
65
 
64
- async function unitLocation(kdtId, root, actor) {
66
+ async function unitLocation(kdtId, root, actor, command) {
65
67
  id(kdtId);
66
68
  if (kdtId === root.context.hq) return root;
67
69
  if (root.context.orgType !== 'hq') {
68
70
  throw sessions.fail('unit_scope_unresolved', '当前店铺没有已核实的连锁总部,不能进入下级组织。');
69
71
  }
70
- const response = await requestApi({ path: 'youzan.shop.basic.get/3.0.0', body: {} },
71
- await snapshotApiEnvironment(actor.pinned, kdtId),
72
- { shopApp: false, identityCheck: false, maxResponseBytes: () => 256 * 1024 });
73
- let envelope;
74
- try { envelope = JSON.parse(response.body); } catch { /* Report a bounded protocol error below. */ }
75
- const data = envelope?.data;
76
- if (!response.ok || envelope?.success !== true || envelope.code !== 200 ||
77
- !data || data.sid !== Number(kdtId) || data.root_kdt_id !== Number(root.context.hq)) {
78
- throw sessions.fail('unit_scope_unresolved', '未能核实目标组织及所属连锁,未改变当前位置。');
72
+ const scope = { kdtId };
73
+ const reject = (category, stage, state, facts) => {
74
+ const error = failure(command, category, stage, scope, state, facts);
75
+ error.cliDetails.nextStep = '未改变当前位置。核对原总部及目标,目录未读完不代表目标不属于该总部;不自动重试或初始化分店应用。';
76
+ return error;
77
+ };
78
+ const { read, binding } = await createBoundShopQuery({ kdtId: Number(root.context.hq) },
79
+ command, 'headquarters-binding', { environment: actor.pinned,
80
+ transportOptions: { shopApp: false, identityCheck: false, maxResponseBytes: () => 256 * 1024 } });
81
+ if (binding.data.role !== 1) {
82
+ throw reject('scope_unresolved', 'headquarters-binding', 'sent', binding.facts);
79
83
  }
84
+ const data = await verifyMembership(read, kdtId, reject, { notFoundCategory: 'permission' });
80
85
  if (![2001, 2002].includes(data.role)) {
81
86
  throw sessions.fail('unit_not_supported', '本版仅支持已核实的连锁门店或网店位置。');
82
87
  }
@@ -135,7 +140,7 @@ async function select(kdtId, options = {}, env = process.env, explicitContext) {
135
140
  const rootId = options.unit ? expected.binding.location?.context.hq : kdtId;
136
141
  if (!rootId) throw sessions.fail('unit_scope_unresolved', '请先选择当前连锁。');
137
142
  const root = await rootLocation(rootId, actor);
138
- const location = options.unit ? await unitLocation(kdtId, root, actor) : root;
143
+ const location = options.unit ? await unitLocation(kdtId, root, actor, 'shop.unit.use') : root;
139
144
  const effect = sessions.commit(location, expected, options, env);
140
145
  return result(options.unit ? 'use_unit' : 'use_root', effect, env);
141
146
  }
@@ -145,7 +150,7 @@ async function resolve(kdtId, options = {}, env = process.env) {
145
150
  const rootId = options.rootKdtId === undefined ? kdtId : id(options.rootKdtId);
146
151
  const actor = await identify(env);
147
152
  const root = await rootLocation(rootId, actor);
148
- const location = await unitLocation(kdtId, root, actor);
153
+ const location = await unitLocation(kdtId, root, actor, 'shop.resolve');
149
154
  return { ...location, effect: { action: 'resolve', currentChanged: false, recentChanged: false },
150
155
  revision: null };
151
156
  }
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const { object } = require('../execution/query-api');
4
+
5
+ const organizationPath = 'youzan.shop.chain.descendent.organization.list/1.0.1';
6
+ const maxOrganizationPages = 20;
7
+ const pageSize = 50;
8
+
9
+ function organizationBody(pageNo) {
10
+ return { page_num: pageNo, page_size: pageSize, with_dept_info: false,
11
+ with_shop_status_and_lifecycle_end_time: false, with_crm_shop: false };
12
+ }
13
+
14
+ async function verifyMembership(read, targetId, reject, { maxPages = maxOrganizationPages,
15
+ notFoundCategory = 'scope_unresolved' } = {}) {
16
+ const seen = new Set();
17
+ let total;
18
+ let lastFacts;
19
+ for (let pageNo = 1; pageNo <= maxPages; pageNo += 1) {
20
+ const { data, facts } = await read(organizationPath, organizationBody(pageNo), 'target-membership');
21
+ lastFacts = facts;
22
+ const invalid = category => reject(category, 'target-membership', 'sent', facts);
23
+ if (!object(data) || !Array.isArray(data.organization_list) ||
24
+ data.page_num !== pageNo || data.page_size !== pageSize ||
25
+ !Number.isSafeInteger(data.total) || data.total < 0 ||
26
+ data.organization_list.length !== Math.min(pageSize, Math.max(0, data.total - seen.size)) ||
27
+ (total !== undefined && total !== data.total)) throw invalid('protocol');
28
+ total = data.total;
29
+ for (const unit of data.organization_list) {
30
+ if (!object(unit) || !Number.isSafeInteger(unit.kdt_id) || unit.kdt_id < 1 ||
31
+ !Number.isSafeInteger(unit.role) || unit.role < 0 || unit.role === 9 ||
32
+ seen.has(unit.kdt_id)) throw invalid('protocol');
33
+ seen.add(unit.kdt_id);
34
+ }
35
+ const member = data.organization_list.find(unit => String(unit.kdt_id) === String(targetId));
36
+ if (member) return member;
37
+ if (seen.size === total) throw invalid(notFoundCategory);
38
+ }
39
+ throw reject('scope_unresolved', 'target-membership', 'sent', lastFacts);
40
+ }
41
+
42
+ module.exports = { organizationPath, organizationBody, maxOrganizationPages, verifyMembership };
@@ -246,7 +246,7 @@ async function readResponseBody(response, maxResponseBytes) {
246
246
  }
247
247
 
248
248
  async function snapshotApiEnvironment(env = apiEnvironment(), kdtId) {
249
- if (env.ZAN_SAAS_SESSION_ID !== undefined && env.ZAN_SAAS_KDT_ID &&
249
+ if (!boundTargets.has(env) && env.ZAN_SAAS_SESSION_ID !== undefined && env.ZAN_SAAS_KDT_ID &&
250
250
  env.ZAN_SAAS_KDT_ID !== String(kdtId)) {
251
251
  throw new ApiConfigurationError('显式请求店铺头与本次命令目标冲突。', 'context_override_conflict');
252
252
  }