@youzan-cloud/zan-saas-cli 0.0.0-stage → 0.1.0-beta.3
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/DEPENDENCIES.json +50 -0
- package/INSTALL.md +79 -0
- package/LICENSE +21 -0
- package/NOTICE +1 -0
- package/README.md +151 -2
- package/THIRD_PARTY_NOTICES.txt +162 -0
- package/bin/zan-saas-cli.js +28 -0
- package/internal/registry/index.js +123 -0
- package/internal/registry/manifest.json +424 -0
- package/npm-shrinkwrap.json +86 -0
- package/package.json +33 -4
- package/skills/README.md +30 -0
- package/skills/zan-saas/SKILL.md +54 -0
- package/skills/zan-saas/references/host-entry.md +25 -0
- package/skills/zan-saas-access/SKILL.md +37 -0
- package/skills/zan-saas-access/references/role-list.md +11 -0
- package/skills/zan-saas-access/references/staff-perms-get.md +11 -0
- package/skills/zan-saas-aftersale/SKILL.md +31 -0
- package/skills/zan-saas-aftersale/references/case-list.md +13 -0
- package/skills/zan-saas-aftersale/references/case-scan.md +31 -0
- package/skills/zan-saas-benefit/SKILL.md +46 -0
- package/skills/zan-saas-benefit/references/card-get.md +7 -0
- package/skills/zan-saas-benefit/references/card-grant-list.md +11 -0
- package/skills/zan-saas-benefit/references/card-list.md +7 -0
- package/skills/zan-saas-benefit/references/customer-card-get.md +7 -0
- package/skills/zan-saas-benefit/references/customer-card-list.md +7 -0
- package/skills/zan-saas-coupon/SKILL.md +41 -0
- package/skills/zan-saas-coupon/references/activity-get.md +11 -0
- package/skills/zan-saas-coupon/references/activity-list.md +15 -0
- package/skills/zan-saas-coupon/references/voucher-get.md +13 -0
- package/skills/zan-saas-coupon/references/voucher-list.md +25 -0
- package/skills/zan-saas-customer/SKILL.md +73 -0
- package/skills/zan-saas-customer/references/customer-scan.md +11 -0
- package/skills/zan-saas-customer/references/segment-list.md +22 -0
- package/skills/zan-saas-fulfillment/SKILL.md +36 -0
- package/skills/zan-saas-fulfillment/references/delivery-get.md +11 -0
- package/skills/zan-saas-fulfillment/references/shipment-list.md +11 -0
- package/skills/zan-saas-fulfillment/references/waybill-get.md +9 -0
- package/skills/zan-saas-interaction/SKILL.md +37 -0
- package/skills/zan-saas-interaction/references/lottery-get.md +11 -0
- package/skills/zan-saas-interaction/references/lottery-list.md +19 -0
- package/skills/zan-saas-item/SKILL.md +61 -0
- package/skills/zan-saas-item/references/attribute-list.md +11 -0
- package/skills/zan-saas-item/references/brand-list.md +7 -0
- package/skills/zan-saas-item/references/classification-get.md +7 -0
- package/skills/zan-saas-item/references/classification-list.md +7 -0
- package/skills/zan-saas-item/references/group-list.md +9 -0
- package/skills/zan-saas-item/references/item-content.md +9 -0
- package/skills/zan-saas-item/references/item-get.md +9 -0
- package/skills/zan-saas-item/references/item-search.md +23 -0
- package/skills/zan-saas-item/references/product-identity.md +11 -0
- package/skills/zan-saas-member/SKILL.md +41 -0
- package/skills/zan-saas-member/references/customer-level-get.md +7 -0
- package/skills/zan-saas-member/references/growth-log-list.md +17 -0
- package/skills/zan-saas-member/references/membership-get.md +7 -0
- package/skills/zan-saas-order/SKILL.md +56 -0
- package/skills/zan-saas-order/references/order-get.md +26 -0
- package/skills/zan-saas-order/references/order-list.md +25 -0
- package/skills/zan-saas-order/references/order-scan.md +35 -0
- package/skills/zan-saas-org/SKILL.md +37 -0
- package/skills/zan-saas-org/references/staff-get.md +15 -0
- package/skills/zan-saas-org/references/staff-role-get.md +11 -0
- package/skills/zan-saas-org/references/unit-get.md +11 -0
- package/skills/zan-saas-org/references/unit-list.md +15 -0
- package/skills/zan-saas-points/SKILL.md +42 -0
- package/skills/zan-saas-points/references/account-get.md +7 -0
- package/skills/zan-saas-points/references/log-list.md +17 -0
- package/skills/zan-saas-points/references/rule-list.md +2 -0
- package/skills/zan-saas-privacy/SKILL.md +30 -0
- package/skills/zan-saas-privacy/references/field-decrypt.md +19 -0
- package/skills/zan-saas-shared/SKILL.md +94 -0
- package/skills/zan-saas-shared/references/authentication.md +52 -0
- package/skills/zan-saas-shared/references/value-processing.md +27 -0
- package/skills/zan-saas-stock/SKILL.md +28 -0
- package/skills/zan-saas-stock/references/log-list.md +11 -0
- package/skills/zan-saas-valuecard/SKILL.md +37 -0
- package/skills/zan-saas-valuecard/references/card-get.md +13 -0
- package/skills/zan-saas-valuecard/references/card-list.md +11 -0
- package/src/auth/browser.js +28 -0
- package/src/auth/callback-server.js +99 -0
- package/src/auth/config.js +53 -0
- package/src/auth/credential-store.js +109 -0
- package/src/auth/oauth-client.js +105 -0
- package/src/auth/pkce.js +28 -0
- package/src/auth/session.js +178 -0
- package/src/auth/tool.js +25 -0
- package/src/cli/command-paths.js +126 -0
- package/src/cli/context-help.js +102 -0
- package/src/cli/diagnostic-command.js +57 -0
- package/src/cli/domain-catalog.js +42 -0
- package/src/cli/help-text.js +32 -0
- package/src/cli/native-loader.js +120 -0
- package/src/cli/native-path.js +22 -0
- package/src/cli/parser-policy.js +141 -0
- package/src/cli/program.js +134 -0
- package/src/cli/skill-catalog.js +241 -0
- package/src/commands/native/access/commands.index.json +24 -0
- package/src/commands/native/access/domain.json +3 -0
- package/src/commands/native/access/role/list/command.js +141 -0
- package/src/commands/native/access/role/list/schema.json +393 -0
- package/src/commands/native/access/staff-perms/get/command.js +119 -0
- package/src/commands/native/access/staff-perms/get/schema.json +703 -0
- package/src/commands/native/aftersale/case/get/command.js +104 -0
- package/src/commands/native/aftersale/case/get/exact-json.js +52 -0
- package/src/commands/native/aftersale/case/get/money.js +28 -0
- package/src/commands/native/aftersale/case/get/projection.js +120 -0
- package/src/commands/native/aftersale/case/get/query.js +72 -0
- package/src/commands/native/aftersale/case/get/schema.json +134 -0
- package/src/commands/native/aftersale/case/list/command.js +135 -0
- package/src/commands/native/aftersale/case/list/schema.json +1095 -0
- package/src/commands/native/aftersale/case/scan/command.js +110 -0
- package/src/commands/native/aftersale/case/scan/query.js +62 -0
- package/src/commands/native/aftersale/case/scan/scan.js +78 -0
- package/src/commands/native/aftersale/case/scan/schema.json +964 -0
- package/src/commands/native/aftersale/case/scan/search.js +108 -0
- package/src/commands/native/aftersale/commands.index.json +42 -0
- package/src/commands/native/aftersale/domain.json +3 -0
- package/src/commands/native/aftersale/refund/list/command.js +97 -0
- package/src/commands/native/aftersale/refund/list/money.js +25 -0
- package/src/commands/native/aftersale/refund/list/projection.js +50 -0
- package/src/commands/native/aftersale/refund/list/query.js +73 -0
- package/src/commands/native/aftersale/refund/list/schema.json +130 -0
- package/src/commands/native/aftersale/shared/query.js +58 -0
- package/src/commands/native/aftersale/shared/search.js +128 -0
- package/src/commands/native/benefit/card-definition/get/command.js +110 -0
- package/src/commands/native/benefit/card-definition/get/schema.json +545 -0
- package/src/commands/native/benefit/card-definition/list/command.js +105 -0
- package/src/commands/native/benefit/card-definition/list/schema.json +690 -0
- package/src/commands/native/benefit/card-grant/list/command.js +135 -0
- package/src/commands/native/benefit/card-grant/list/schema.json +758 -0
- package/src/commands/native/benefit/commands.index.json +51 -0
- package/src/commands/native/benefit/customer-card/get/command.js +110 -0
- package/src/commands/native/benefit/customer-card/get/schema.json +340 -0
- package/src/commands/native/benefit/customer-card/list/command.js +102 -0
- package/src/commands/native/benefit/customer-card/list/schema.json +731 -0
- package/src/commands/native/benefit/domain.json +3 -0
- package/src/commands/native/coupon/activity/get/command.js +183 -0
- package/src/commands/native/coupon/activity/get/schema.json +624 -0
- package/src/commands/native/coupon/activity/list/command.js +169 -0
- package/src/commands/native/coupon/activity/list/schema.json +845 -0
- package/src/commands/native/coupon/commands.index.json +42 -0
- package/src/commands/native/coupon/domain.json +3 -0
- package/src/commands/native/coupon/shared/query.js +15 -0
- package/src/commands/native/coupon/voucher/get/command.js +113 -0
- package/src/commands/native/coupon/voucher/get/schema.json +147 -0
- package/src/commands/native/coupon/voucher/list/command.js +155 -0
- package/src/commands/native/coupon/voucher/list/schema.json +772 -0
- package/src/commands/native/customer/commands.index.json +96 -0
- package/src/commands/native/customer/customer/get/adapter.js +82 -0
- package/src/commands/native/customer/customer/get/command.js +106 -0
- package/src/commands/native/customer/customer/get/schema.json +958 -0
- package/src/commands/native/customer/customer/list/command.js +78 -0
- package/src/commands/native/customer/customer/list/input.js +145 -0
- package/src/commands/native/customer/customer/list/query.js +103 -0
- package/src/commands/native/customer/customer/list/schema.json +969 -0
- package/src/commands/native/customer/customer/scan/command.js +80 -0
- package/src/commands/native/customer/customer/scan/scan.js +92 -0
- package/src/commands/native/customer/customer/scan/schema.json +211 -0
- package/src/commands/native/customer/customer-tag/get/command.js +166 -0
- package/src/commands/native/customer/customer-tag/get/schema.json +317 -0
- package/src/commands/native/customer/customer-tag/list/command.js +143 -0
- package/src/commands/native/customer/customer-tag/list/schema.json +259 -0
- package/src/commands/native/customer/domain.json +3 -0
- package/src/commands/native/customer/segment/list/command.js +96 -0
- package/src/commands/native/customer/segment/list/query.js +98 -0
- package/src/commands/native/customer/segment/list/schema.json +135 -0
- package/src/commands/native/customer/segment-customer/list/command.js +171 -0
- package/src/commands/native/customer/segment-customer/list/schema.json +123 -0
- package/src/commands/native/customer/shared/search-input.js +179 -0
- package/src/commands/native/customer/shared/search-page.js +54 -0
- package/src/commands/native/customer/tag/get/command.js +73 -0
- package/src/commands/native/customer/tag/get/schema.json +339 -0
- package/src/commands/native/customer/tagcategory/get/command.js +80 -0
- package/src/commands/native/customer/tagcategory/get/query.js +98 -0
- package/src/commands/native/customer/tagcategory/get/schema.json +339 -0
- package/src/commands/native/customer/tagcategory/list/command.js +127 -0
- package/src/commands/native/customer/tagcategory/list/schema.json +394 -0
- package/src/commands/native/fulfillment/commands.index.json +33 -0
- package/src/commands/native/fulfillment/delivery/get/command.js +107 -0
- package/src/commands/native/fulfillment/delivery/get/projection.js +142 -0
- package/src/commands/native/fulfillment/delivery/get/schema.json +173 -0
- package/src/commands/native/fulfillment/domain.json +3 -0
- package/src/commands/native/fulfillment/shipment/list/command.js +83 -0
- package/src/commands/native/fulfillment/shipment/list/projection.js +130 -0
- package/src/commands/native/fulfillment/shipment/list/schema.json +752 -0
- package/src/commands/native/fulfillment/waybill/get/command.js +193 -0
- package/src/commands/native/fulfillment/waybill/get/response.js +68 -0
- package/src/commands/native/fulfillment/waybill/get/schema.json +190 -0
- package/src/commands/native/interaction/commands.index.json +24 -0
- package/src/commands/native/interaction/domain.json +3 -0
- package/src/commands/native/interaction/lottery/get/command.js +61 -0
- package/src/commands/native/interaction/lottery/get/query.js +152 -0
- package/src/commands/native/interaction/lottery/get/schema.json +1004 -0
- package/src/commands/native/interaction/lottery/list/command.js +176 -0
- package/src/commands/native/interaction/lottery/list/schema.json +898 -0
- package/src/commands/native/item/attribute/list/command.js +122 -0
- package/src/commands/native/item/attribute/list/schema.json +727 -0
- package/src/commands/native/item/brand/list/command.js +114 -0
- package/src/commands/native/item/brand/list/schema.json +725 -0
- package/src/commands/native/item/classification/get/command.js +87 -0
- package/src/commands/native/item/classification/get/schema.json +596 -0
- package/src/commands/native/item/classification/list/command.js +105 -0
- package/src/commands/native/item/classification/list/schema.json +676 -0
- package/src/commands/native/item/commands.index.json +87 -0
- package/src/commands/native/item/domain.json +1 -0
- package/src/commands/native/item/group/list/command.js +122 -0
- package/src/commands/native/item/group/list/schema.json +809 -0
- package/src/commands/native/item/item/get/command.js +122 -0
- package/src/commands/native/item/item/get/schema.json +3799 -0
- package/src/commands/native/item/item/list/command.js +7 -0
- package/src/commands/native/item/item/list/schema.json +1118 -0
- package/src/commands/native/item/item/scan/command.js +7 -0
- package/src/commands/native/item/item/scan/schema.json +1046 -0
- package/src/commands/native/item/item-content/get/command.js +54 -0
- package/src/commands/native/item/item-content/get/schema.json +677 -0
- package/src/commands/native/item/shared/api.js +31 -0
- package/src/commands/native/item/shared/content.js +20 -0
- package/src/commands/native/item/shared/diagnostics.js +61 -0
- package/src/commands/native/item/shared/h5.js +63 -0
- package/src/commands/native/item/shared/identity.js +62 -0
- package/src/commands/native/item/shared/normalize.js +49 -0
- package/src/commands/native/item/shared/scope.js +22 -0
- package/src/commands/native/item/shared/search-command.js +61 -0
- package/src/commands/native/item/shared/search-options.js +150 -0
- package/src/commands/native/item/shared/search.js +158 -0
- package/src/commands/native/member/commands.index.json +60 -0
- package/src/commands/native/member/customer-level/get/command.js +122 -0
- package/src/commands/native/member/customer-level/get/schema.json +300 -0
- package/src/commands/native/member/domain.json +3 -0
- package/src/commands/native/member/growth/get/command.js +64 -0
- package/src/commands/native/member/growth/get/schema.json +283 -0
- package/src/commands/native/member/growth-log/list/command.js +203 -0
- package/src/commands/native/member/growth-log/list/schema.json +695 -0
- package/src/commands/native/member/level/get/command.js +70 -0
- package/src/commands/native/member/level/get/query.js +125 -0
- package/src/commands/native/member/level/get/schema.json +339 -0
- package/src/commands/native/member/level/list/command.js +50 -0
- package/src/commands/native/member/level/list/query.js +109 -0
- package/src/commands/native/member/level/list/schema.json +104 -0
- package/src/commands/native/member/membership/get/command.js +78 -0
- package/src/commands/native/member/membership/get/schema.json +283 -0
- package/src/commands/native/order/commands.index.json +33 -0
- package/src/commands/native/order/domain.json +3 -0
- package/src/commands/native/order/order/get/command.js +155 -0
- package/src/commands/native/order/order/get/schema.json +1167 -0
- package/src/commands/native/order/order/list/command.js +230 -0
- package/src/commands/native/order/order/list/schema.json +1363 -0
- package/src/commands/native/order/order/scan/command.js +148 -0
- package/src/commands/native/order/order/scan/scan.js +89 -0
- package/src/commands/native/order/order/scan/schema.json +1445 -0
- package/src/commands/native/order/shared/projection.js +183 -0
- package/src/commands/native/order/shared/query.js +84 -0
- package/src/commands/native/order/shared/search.js +112 -0
- package/src/commands/native/org/commands.index.json +42 -0
- package/src/commands/native/org/domain.json +3 -0
- package/src/commands/native/org/shared/single-staff.js +115 -0
- package/src/commands/native/org/staff/get/command.js +148 -0
- package/src/commands/native/org/staff/get/schema.json +114 -0
- package/src/commands/native/org/staff-role/get/command.js +45 -0
- package/src/commands/native/org/staff-role/get/schema.json +93 -0
- package/src/commands/native/org/unit/get/command.js +131 -0
- package/src/commands/native/org/unit/get/schema.json +478 -0
- package/src/commands/native/org/unit/list/command.js +187 -0
- package/src/commands/native/org/unit/list/schema.json +828 -0
- package/src/commands/native/points/account/get/command.js +65 -0
- package/src/commands/native/points/account/get/schema.json +303 -0
- package/src/commands/native/points/commands.index.json +33 -0
- package/src/commands/native/points/domain.json +3 -0
- package/src/commands/native/points/log/list/command.js +141 -0
- package/src/commands/native/points/log/list/schema.json +761 -0
- package/src/commands/native/points/rule/list/command.js +82 -0
- package/src/commands/native/points/rule/list/schema.json +294 -0
- package/src/commands/native/privacy/commands.index.json +15 -0
- package/src/commands/native/privacy/domain.json +3 -0
- package/src/commands/native/privacy/field/decrypt/command.js +185 -0
- package/src/commands/native/privacy/field/decrypt/schema.json +656 -0
- package/src/commands/native/privacy/shared/order-source.js +33 -0
- package/src/commands/native/stock/commands.index.json +15 -0
- package/src/commands/native/stock/domain.json +3 -0
- package/src/commands/native/stock/log/list/command.js +207 -0
- package/src/commands/native/stock/log/list/schema.json +922 -0
- package/src/commands/native/valuecard/card/get/command.js +107 -0
- package/src/commands/native/valuecard/card/get/schema.json +683 -0
- package/src/commands/native/valuecard/card/list/command.js +136 -0
- package/src/commands/native/valuecard/card/list/pagination.js +23 -0
- package/src/commands/native/valuecard/card/list/schema.json +780 -0
- package/src/commands/native/valuecard/commands.index.json +24 -0
- package/src/commands/native/valuecard/domain.json +3 -0
- package/src/commands/native/valuecard/shared/projection.js +57 -0
- package/src/commands/system/api.js +47 -0
- package/src/commands/system/auth.js +118 -0
- package/src/commands/system/contracts.js +373 -0
- package/src/commands/system/doctor.js +24 -0
- package/src/commands/system/install.js +41 -0
- package/src/commands/system/printer.js +16 -0
- package/src/commands/system/schema.js +162 -0
- package/src/commands/system/shop-guidance.js +9 -0
- package/src/commands/system/shop.js +237 -0
- package/src/commands/system/skills.js +49 -0
- package/src/execution/beijing-date-time.js +29 -0
- package/src/execution/beijing-time.js +21 -0
- package/src/execution/bounded-input.js +42 -0
- package/src/execution/command-io.js +137 -0
- package/src/execution/command-policy.js +41 -0
- package/src/execution/command-schema.js +131 -0
- package/src/execution/datetime.js +83 -0
- package/src/execution/error-details.js +399 -0
- package/src/execution/error-model.js +82 -0
- package/src/execution/exact-json.js +80 -0
- package/src/execution/list-total.js +10 -0
- package/src/execution/money.js +59 -0
- package/src/execution/parser-error.js +66 -0
- package/src/execution/privacy.js +197 -0
- package/src/execution/public-schema.js +52 -0
- package/src/execution/query-api.js +101 -0
- package/src/execution/range.js +84 -0
- package/src/execution/request-state.js +55 -0
- package/src/execution/schema-completeness.js +116 -0
- package/src/execution/shop-query.js +54 -0
- package/src/execution/target-shop-default.js +23 -0
- package/src/execution/target-shop.js +49 -0
- package/src/identity/context-store.js +54 -0
- package/src/identity/context.js +38 -0
- package/src/installation/install.js +158 -0
- package/src/installation/npm-error.js +93 -0
- package/src/installation/skill.js +113 -0
- package/src/transport/account-api.js +152 -0
- package/src/transport/api.js +199 -0
- package/src/transport/shop-app.js +63 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requestApi } = require('./api');
|
|
4
|
+
|
|
5
|
+
const paths = {
|
|
6
|
+
userInfo: 'youzan.user.info.get/1.0.0',
|
|
7
|
+
shopList: 'youzan.shop.summary.list/1.0.0',
|
|
8
|
+
cloudUser: 'youzan.cloud.user.create/1.0.0',
|
|
9
|
+
shopAppInit: 'youzan.cloud.shop.app.init/1.0.0',
|
|
10
|
+
shopAppGet: 'youzan.cloud.shop.app.get/1.0.0',
|
|
11
|
+
staffStatus: 'youzan.shop.staff.status.get/1.0.0'
|
|
12
|
+
};
|
|
13
|
+
// Staff status of the logged-in user in a shop: 0 正常, 3 停用.
|
|
14
|
+
const staffStatuses = Object.freeze({ active: 0, disabled: 3 });
|
|
15
|
+
// Shop list requires business lines. Default: 微商城 10000, 教育 20000, 酒店 30000, 餐饮 50000,
|
|
16
|
+
// 美业 60000, 零售 70000.
|
|
17
|
+
const defaultLineOfBusinesses = Object.freeze([10000, 20000, 30000, 50000, 60000, 70000]);
|
|
18
|
+
const shopRoles = Object.freeze({
|
|
19
|
+
0: '单店', 1: '总部', 2: '门店/网店', 3: '仓库', 4: '合伙人', 5: '第三方店铺', 6: '分销供货商店铺', 7: '前置仓'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
class AccountApiError extends Error {
|
|
23
|
+
constructor(message, { httpStatus, code, traceId } = {}) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.httpStatus = httpStatus;
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.traceId = traceId;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parse(text) {
|
|
32
|
+
try {
|
|
33
|
+
const value = JSON.parse(text);
|
|
34
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const scalar = value => (typeof value === 'string' || Number.isFinite(value) ? String(value) : undefined);
|
|
41
|
+
|
|
42
|
+
function failure(path, httpStatus, body) {
|
|
43
|
+
const source = body?.gw_err_resp && typeof body.gw_err_resp === 'object' ? body.gw_err_resp : body || {};
|
|
44
|
+
const code = scalar(source.code ?? source.err_code);
|
|
45
|
+
const message = scalar(source.message ?? source.err_msg);
|
|
46
|
+
const traceId = scalar(body?.trace_id ?? source.trace_id);
|
|
47
|
+
const parts = [`HTTP ${httpStatus}`];
|
|
48
|
+
if (code !== undefined) parts.push(`code ${code}`);
|
|
49
|
+
if (message) parts.push(message);
|
|
50
|
+
if (traceId) parts.push(`trace_id ${traceId}`);
|
|
51
|
+
let hint = '';
|
|
52
|
+
if (httpStatus === 401) hint = '请执行 zan-saas auth login 重新登录。';
|
|
53
|
+
else if (httpStatus === 404 && code === '4003') {
|
|
54
|
+
hint = '请先核对 CLI 版本、ZAN_SAAS_ACCOUNT_API_BASE_URL 和服务端路由;不要反复登录或切店。';
|
|
55
|
+
}
|
|
56
|
+
return new AccountApiError(`账号接口 ${path.split('/')[0]} 调用失败:${parts.join(',')}。${hint}`,
|
|
57
|
+
{ httpStatus, code, traceId });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Account APIs answer with a PlainResult envelope; anything else is reported with code and trace_id.
|
|
61
|
+
async function callAccountApi(path, body, env = process.env, options = {}) {
|
|
62
|
+
const response = await requestApi({ path, body }, env, { ...options, gateway: 'account' });
|
|
63
|
+
const json = parse(response.body);
|
|
64
|
+
if (!response.ok || !json || json.success !== true) throw failure(path, response.httpStatus, json);
|
|
65
|
+
return json.data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Long IDs arrive as JSON numbers or digit strings; unsafe numbers are rejected rather than rounded.
|
|
69
|
+
function idString(value) {
|
|
70
|
+
if (Number.isSafeInteger(value) && value > 0) return String(value);
|
|
71
|
+
if (typeof value === 'string' && /^[1-9]\d*$/.test(value)) return value;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function protocol(path) {
|
|
76
|
+
return new AccountApiError(`账号接口 ${path.split('/')[0]} 返回了无法识别的数据。`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const textOrNull = value => (typeof value === 'string' ? value : null);
|
|
80
|
+
|
|
81
|
+
// Keeps the first 3 and last 4 digits; short values are fully masked.
|
|
82
|
+
function maskMobile(value) {
|
|
83
|
+
if (typeof value !== 'string' || !value) return null;
|
|
84
|
+
if (value.length < 8) return '*'.repeat(value.length);
|
|
85
|
+
return `${value.slice(0, 3)}${'*'.repeat(value.length - 7)}${value.slice(-4)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function whoami(env = process.env) {
|
|
89
|
+
const data = await callAccountApi(paths.userInfo, {}, env);
|
|
90
|
+
const userId = idString(data?.userId);
|
|
91
|
+
if (!userId) throw protocol(paths.userInfo);
|
|
92
|
+
return {
|
|
93
|
+
userId, nickName: textOrNull(data.nickName), mobile: maskMobile(data.mobile),
|
|
94
|
+
countryCode: scalar(data.countryCode) ?? null
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Only HQs and single shops can be selected as a context for now.
|
|
99
|
+
function orgTypeOf(shopRole) {
|
|
100
|
+
return shopRole === 1 ? 'hq' : shopRole === 0 ? 'shop' : null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function shopItem(item) {
|
|
104
|
+
const kdtId = idString(item?.kdtId);
|
|
105
|
+
if (!kdtId) return null;
|
|
106
|
+
const shopRole = Number.isSafeInteger(item.shopRole) ? item.shopRole : null;
|
|
107
|
+
const lifeCycleStatus = item.lifeCycleStatus;
|
|
108
|
+
return {
|
|
109
|
+
kdtId, shopName: textOrNull(item.shopName), shopRole, orgType: orgTypeOf(shopRole),
|
|
110
|
+
lifeCycleStatus: typeof lifeCycleStatus === 'string' || Number.isSafeInteger(lifeCycleStatus)
|
|
111
|
+
? lifeCycleStatus : null
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function listShops({
|
|
116
|
+
name, pageNum = 1, pageSize = 20, lineOfBusinesses = defaultLineOfBusinesses, includeKdtIds
|
|
117
|
+
} = {}, env = process.env) {
|
|
118
|
+
const body = {
|
|
119
|
+
onlyShowCdpGroupShop: false, lineOfBusinesses: [...lineOfBusinesses], pageNum, pageSize,
|
|
120
|
+
...(name ? { shopName: name } : {}),
|
|
121
|
+
...(includeKdtIds ? { includeKdtIds: includeKdtIds.map(Number) } : {})
|
|
122
|
+
};
|
|
123
|
+
const data = await callAccountApi(paths.shopList, body, env);
|
|
124
|
+
const list = data?.data ?? [];
|
|
125
|
+
if (!data || typeof data !== 'object' || !Array.isArray(list)) throw protocol(paths.shopList);
|
|
126
|
+
const items = list.map(shopItem);
|
|
127
|
+
if (items.includes(null)) throw protocol(paths.shopList);
|
|
128
|
+
const positiveInteger = (value, fallback) => (Number.isSafeInteger(value) && value > 0 ? value : fallback);
|
|
129
|
+
return {
|
|
130
|
+
total: Number.isSafeInteger(data.total) && data.total >= 0 ? data.total : null,
|
|
131
|
+
pageNum: positiveInteger(data.pageNum, pageNum), pageSize: positiveInteger(data.pageSize, pageSize), items
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The gateway injects the user from the token; null means the shop has no staff record of this user.
|
|
136
|
+
async function staffStatus(kdtId, env = process.env) {
|
|
137
|
+
const data = await callAccountApi(paths.staffStatus, { kdtId: Number(kdtId) }, env);
|
|
138
|
+
if (data === undefined || data === null) return null;
|
|
139
|
+
if (typeof data !== 'object') throw protocol(paths.staffStatus);
|
|
140
|
+
return Number.isSafeInteger(data.status) ? data.status : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function ensureCloudUser(env = process.env, { timeoutMs = 10000 } = {}) {
|
|
144
|
+
const data = await callAccountApi(paths.cloudUser, {}, env, { timeoutMs });
|
|
145
|
+
if (!data || typeof data !== 'object') throw protocol(paths.cloudUser);
|
|
146
|
+
return { userId: idString(data.userId) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
AccountApiError, callAccountApi, defaultLineOfBusinesses, ensureCloudUser, listShops, maskMobile,
|
|
151
|
+
orgTypeOf, paths, shopRoles, staffStatus, staffStatuses, whoami
|
|
152
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
4
|
+
const { resolveAccessToken, AuthRequiredError } = require('../auth/session');
|
|
5
|
+
const {
|
|
6
|
+
AuthConfigurationError, defaultAccountApiBaseUrl, defaultApiBaseUrl, insecureAllowed
|
|
7
|
+
} = require('../auth/config');
|
|
8
|
+
const { readContext } = require('../identity/context');
|
|
9
|
+
const { contextInput } = require('../identity/context-store');
|
|
10
|
+
const { runWithRequestState, beginRequest } = require('../execution/request-state');
|
|
11
|
+
|
|
12
|
+
const apiPathPattern = /^\/?([a-z][a-z0-9._-]*)\/([0-9]+\.[0-9]+\.[0-9]+)$/;
|
|
13
|
+
const apiSessions = new AsyncLocalStorage();
|
|
14
|
+
|
|
15
|
+
function runWithApiSession(action, environment) {
|
|
16
|
+
return runWithRequestState(() =>
|
|
17
|
+
apiSessions.run({ environment, env: undefined, credential: undefined }, action));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function apiEnvironment() {
|
|
21
|
+
const session = apiSessions.getStore();
|
|
22
|
+
if (!session) return Object.freeze({ ...process.env });
|
|
23
|
+
session.env ||= Object.freeze({ ...(session.environment || process.env) });
|
|
24
|
+
return session.env;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Business APIs select the shop per request; account APIs serve the user and send no shop.
|
|
28
|
+
const gateways = {
|
|
29
|
+
business: { key: 'ZAN_SAAS_API_BASE_URL', base: defaultApiBaseUrl, label: 'API base URL', shop: true },
|
|
30
|
+
account: {
|
|
31
|
+
key: 'ZAN_SAAS_ACCOUNT_API_BASE_URL', base: defaultAccountApiBaseUrl, label: 'Account API base URL',
|
|
32
|
+
shop: false
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
class ApiConfigurationError extends Error {
|
|
37
|
+
constructor(message, reason = 'invalid_configuration') {
|
|
38
|
+
super(message);
|
|
39
|
+
this.diagnosticReason = reason;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
class ApiResponseLimitError extends Error {
|
|
43
|
+
constructor(httpStatus) {
|
|
44
|
+
super('API response exceeds byte limit');
|
|
45
|
+
this.httpStatus = httpStatus;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function accessToken(env) {
|
|
50
|
+
try {
|
|
51
|
+
const session = apiSessions.getStore();
|
|
52
|
+
if (session && env === session.env) {
|
|
53
|
+
session.credential ||= resolveAccessToken(env);
|
|
54
|
+
return (await session.credential).token;
|
|
55
|
+
}
|
|
56
|
+
return (await resolveAccessToken(env)).token;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof AuthRequiredError || error instanceof AuthConfigurationError) {
|
|
59
|
+
throw new ApiConfigurationError(error.message,
|
|
60
|
+
error instanceof AuthRequiredError ? 'missing_authentication' : 'invalid_configuration');
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Personal tokens select the shop per request: ZAN_SAAS_KDT_ID, else the shopId of ZAN_SAAS_CONTEXT,
|
|
67
|
+
// else the shop saved by `zan-saas shop use`.
|
|
68
|
+
function shopHeader(env) {
|
|
69
|
+
if (env.ZAN_SAAS_KDT_ID) {
|
|
70
|
+
if (!/^[1-9]\d*$/.test(env.ZAN_SAAS_KDT_ID)) {
|
|
71
|
+
throw new ApiConfigurationError('ZAN_SAAS_KDT_ID must be a positive integer ID');
|
|
72
|
+
}
|
|
73
|
+
return env.ZAN_SAAS_KDT_ID;
|
|
74
|
+
}
|
|
75
|
+
const state = readContext(contextInput(env));
|
|
76
|
+
return state.status === 'ready' ? state.context.shopId : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function endpoint(path, env, gateway = gateways.business) {
|
|
80
|
+
const base = env[gateway.key] || gateway.base;
|
|
81
|
+
const match = apiPathPattern.exec(path);
|
|
82
|
+
if (!match) throw new ApiConfigurationError('API path must be <api-name>/<version>');
|
|
83
|
+
let url;
|
|
84
|
+
try { url = new URL(base); }
|
|
85
|
+
catch { throw new ApiConfigurationError(`${gateway.label} must be a valid HTTPS URL`); }
|
|
86
|
+
const plainHttp = url.protocol === 'http:' && insecureAllowed(url, env);
|
|
87
|
+
if ((!plainHttp && url.protocol !== 'https:') || url.username ||
|
|
88
|
+
url.password || url.search || url.hash || url.origin === 'null') {
|
|
89
|
+
throw new ApiConfigurationError(`${gateway.label} must be an HTTPS URL without credentials, query or fragment`);
|
|
90
|
+
}
|
|
91
|
+
// Resolve the token last so invalid configuration never triggers a login or refresh.
|
|
92
|
+
const token = await accessToken(env);
|
|
93
|
+
const requestUrl = new URL(
|
|
94
|
+
`${url.pathname.replace(/\/$/, '')}/${match[1]}/${match[2]}`, url.origin
|
|
95
|
+
);
|
|
96
|
+
requestUrl.searchParams.set('access_token', token);
|
|
97
|
+
return { requestUrl, token };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function requestApi({ path, body }, env = apiEnvironment(), options = {}) {
|
|
101
|
+
const target = gateways[options.gateway || 'business'];
|
|
102
|
+
if (!target) throw new ApiConfigurationError(`Unknown API gateway: ${options.gateway}`);
|
|
103
|
+
const kdtId = target.shop ? shopHeader(env) : undefined;
|
|
104
|
+
const send = () => sendApi(path, body, env, target, kdtId, options);
|
|
105
|
+
const response = await send();
|
|
106
|
+
if (!kdtId || env.ZAN_SAAS_SHOP_APP_INIT === '0' || options.shopApp === false) return response;
|
|
107
|
+
// Loaded lazily: shop-app calls the account gateway through this module.
|
|
108
|
+
const shopApp = require('./shop-app');
|
|
109
|
+
if (!shopApp.isNoAppResponse(response)) return response;
|
|
110
|
+
return shopApp.recoverNoApp(kdtId, env, send, response, options.shopApp);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function sendApi(path, body, env, target, kdtId, { onRequest, maxResponseBytes, timeoutMs = 10000 }) {
|
|
114
|
+
const { requestUrl, token } = await endpoint(path, env, target);
|
|
115
|
+
const encodedBody = JSON.stringify(body);
|
|
116
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
117
|
+
let response;
|
|
118
|
+
if (onRequest) onRequest();
|
|
119
|
+
const finish = beginRequest();
|
|
120
|
+
try {
|
|
121
|
+
response = await fetch(requestUrl, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
'content-type': 'application/json',
|
|
125
|
+
...(kdtId ? { 'youzanyun-kdtid': kdtId } : {})
|
|
126
|
+
},
|
|
127
|
+
body: encodedBody,
|
|
128
|
+
redirect: 'manual',
|
|
129
|
+
signal
|
|
130
|
+
});
|
|
131
|
+
} catch {
|
|
132
|
+
finish?.('unknown');
|
|
133
|
+
throw new Error('API request failed or timed out; delivery status is unknown');
|
|
134
|
+
}
|
|
135
|
+
let text;
|
|
136
|
+
let responseBytes;
|
|
137
|
+
try {
|
|
138
|
+
const result = await readResponseBody(response, maxResponseBytes);
|
|
139
|
+
text = result.text;
|
|
140
|
+
responseBytes = result.responseBytes;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
finish?.(error instanceof ApiResponseLimitError ? 'sent' : 'unknown');
|
|
143
|
+
if (error instanceof ApiResponseLimitError) throw error;
|
|
144
|
+
throw new Error('API response was interrupted; request outcome is unknown');
|
|
145
|
+
}
|
|
146
|
+
finish?.('sent');
|
|
147
|
+
// Never return the credential, even if an upstream response echoes it.
|
|
148
|
+
const redacted = text.replaceAll(token, '[REDACTED]')
|
|
149
|
+
.replaceAll(encodeURIComponent(token), '[REDACTED]')
|
|
150
|
+
.replaceAll(requestUrl.searchParams.toString().slice('access_token='.length), '[REDACTED]');
|
|
151
|
+
return { httpStatus: response.status, ok: response.ok, body: redacted,
|
|
152
|
+
responseBytes: responseBytes ?? Buffer.byteLength(text, 'utf8') };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function readResponseBody(response, maxResponseBytes) {
|
|
156
|
+
if (maxResponseBytes === undefined) {
|
|
157
|
+
const text = await response.text();
|
|
158
|
+
return { text, responseBytes: Buffer.byteLength(text, 'utf8') };
|
|
159
|
+
}
|
|
160
|
+
const limit = maxResponseBytes();
|
|
161
|
+
// A response with a trustworthy, uncompressed Content-Length can be
|
|
162
|
+
// rejected before reading a large body. This keeps the byte-budget failure
|
|
163
|
+
// deterministic even when the producer is slow or the body is much larger
|
|
164
|
+
// than the command limit.
|
|
165
|
+
const contentLength = response.headers?.get('content-length');
|
|
166
|
+
const contentEncoding = response.headers?.get('content-encoding');
|
|
167
|
+
if (!contentEncoding && /^\d+$/.test(contentLength || '') &&
|
|
168
|
+
Number(contentLength) > limit) {
|
|
169
|
+
throw new ApiResponseLimitError(response.status);
|
|
170
|
+
}
|
|
171
|
+
const reader = response.body?.getReader();
|
|
172
|
+
const chunks = [];
|
|
173
|
+
let bytes = 0;
|
|
174
|
+
if (reader) {
|
|
175
|
+
try {
|
|
176
|
+
while (true) {
|
|
177
|
+
const { done, value } = await reader.read();
|
|
178
|
+
if (done) break;
|
|
179
|
+
bytes += value.byteLength;
|
|
180
|
+
if (bytes > limit) {
|
|
181
|
+
try { await reader.cancel(); } catch {}
|
|
182
|
+
throw new ApiResponseLimitError(response.status);
|
|
183
|
+
}
|
|
184
|
+
chunks.push(value);
|
|
185
|
+
}
|
|
186
|
+
} finally { reader.releaseLock(); }
|
|
187
|
+
}
|
|
188
|
+
return { text: Buffer.concat(chunks, bytes).toString('utf8'), responseBytes: bytes };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function snapshotApiEnvironment(env = apiEnvironment(), kdtId) {
|
|
192
|
+
const { token } = await endpoint('youzan.shop.basic.get/3.0.0', env);
|
|
193
|
+
return Object.freeze({ ...env, ZAN_SAAS_USER_ACCESS_TOKEN: token, ZAN_SAAS_KDT_ID: String(kdtId) });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
requestApi, snapshotApiEnvironment, runWithApiSession, ApiConfigurationError,
|
|
198
|
+
ApiResponseLimitError, readResponseBody
|
|
199
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { callAccountApi, paths } = require('./account-api');
|
|
4
|
+
|
|
5
|
+
// The business gateway answers 1106 when the selected shop has no usable app. The request never
|
|
6
|
+
// reached the API, so it is safe to send again once the app is ready.
|
|
7
|
+
const noAppCode = '1106';
|
|
8
|
+
// CloudAppStateEnums: only 2 is ready; 4, 5 and 6 never become ready without a person stepping in.
|
|
9
|
+
const appStates = Object.freeze({ 1: '审核中', 2: '已创建', 3: '初始化中', 4: '初始化失败', 5: '审核驳回', 6: '已删除' });
|
|
10
|
+
const readyState = 2;
|
|
11
|
+
const finalStates = new Set([4, 5, 6]);
|
|
12
|
+
const defaultPolicy = Object.freeze({ attempts: 10, intervalMs: 3000, initTimeoutMs: 20000, queryTimeoutMs: 10000 });
|
|
13
|
+
|
|
14
|
+
function isNoAppResponse(response) {
|
|
15
|
+
let json;
|
|
16
|
+
try { json = JSON.parse(response.body); } catch { return false; }
|
|
17
|
+
if (!json || typeof json !== 'object') return false;
|
|
18
|
+
const source = json.gw_err_resp && typeof json.gw_err_resp === 'object' ? json.gw_err_resp : json;
|
|
19
|
+
return String(source.code ?? source.err_code) === noAppCode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const stateText = app => (app ? `应用 ${app.appId} ${appStates[app.appState] || `状态 ${app.appState}`}` : '应用尚未生成');
|
|
23
|
+
const sleepFor = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
24
|
+
|
|
25
|
+
// Initializes the shop app once, polls until it is ready and resends the request. Any failure returns
|
|
26
|
+
// the last 1106 response so callers report it as usual; the reason goes to stderr.
|
|
27
|
+
async function recoverNoApp(kdtId, env, resend, firstResponse, {
|
|
28
|
+
attempts = defaultPolicy.attempts, intervalMs = defaultPolicy.intervalMs,
|
|
29
|
+
initTimeoutMs = defaultPolicy.initTimeoutMs, queryTimeoutMs = defaultPolicy.queryTimeoutMs,
|
|
30
|
+
notify = message => process.stderr.write(`${message}\n`), sleep = sleepFor
|
|
31
|
+
} = {}) {
|
|
32
|
+
notify(`店铺 ${kdtId} 下没有可用的应用,正在初始化。`);
|
|
33
|
+
try {
|
|
34
|
+
await callAccountApi(paths.shopAppInit, { kdtId: Number(kdtId) }, env, { timeoutMs: initTimeoutMs });
|
|
35
|
+
} catch (error) {
|
|
36
|
+
notify(`应用初始化失败:${error.message}`);
|
|
37
|
+
return firstResponse;
|
|
38
|
+
}
|
|
39
|
+
let last = firstResponse;
|
|
40
|
+
let app = null;
|
|
41
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
42
|
+
await sleep(intervalMs);
|
|
43
|
+
try {
|
|
44
|
+
app = await callAccountApi(paths.shopAppGet, { kdtId: Number(kdtId) }, env, { timeoutMs: queryTimeoutMs });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
notify(`查询应用状态失败:${error.message}`);
|
|
47
|
+
return last;
|
|
48
|
+
}
|
|
49
|
+
if (app && finalStates.has(app.appState)) {
|
|
50
|
+
notify(`店铺 ${kdtId} 的${stateText(app)},需要到有赞云控制台处理后重试。`);
|
|
51
|
+
return last;
|
|
52
|
+
}
|
|
53
|
+
if (app && app.appState === readyState) {
|
|
54
|
+
last = await resend();
|
|
55
|
+
if (!isNoAppResponse(last)) return last;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
notify(`店铺 ${kdtId} 的应用还没生效:已查询 ${attempts} 次(约 ${Math.round(attempts * intervalMs / 1000)} 秒),` +
|
|
59
|
+
`${stateText(app)}。稍后重试;有容器应用要等审核通过。`);
|
|
60
|
+
return last;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { appStates, defaultPolicy, isNoAppResponse, noAppCode, recoverNoApp };
|