@youzan-cloud/zan-saas-cli 0.0.0-stage → 0.1.0-beta.4

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 (329) hide show
  1. package/DEPENDENCIES.json +50 -0
  2. package/INSTALL.md +79 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE +1 -0
  5. package/README.md +151 -2
  6. package/THIRD_PARTY_NOTICES.txt +162 -0
  7. package/bin/zan-saas-cli.js +28 -0
  8. package/internal/registry/index.js +123 -0
  9. package/internal/registry/manifest.json +424 -0
  10. package/npm-shrinkwrap.json +86 -0
  11. package/package.json +33 -4
  12. package/skills/README.md +30 -0
  13. package/skills/zan-saas/SKILL.md +54 -0
  14. package/skills/zan-saas/references/host-entry.md +25 -0
  15. package/skills/zan-saas-access/SKILL.md +37 -0
  16. package/skills/zan-saas-access/references/role-list.md +11 -0
  17. package/skills/zan-saas-access/references/staff-perms-get.md +11 -0
  18. package/skills/zan-saas-aftersale/SKILL.md +31 -0
  19. package/skills/zan-saas-aftersale/references/case-list.md +13 -0
  20. package/skills/zan-saas-aftersale/references/case-scan.md +31 -0
  21. package/skills/zan-saas-benefit/SKILL.md +46 -0
  22. package/skills/zan-saas-benefit/references/card-get.md +7 -0
  23. package/skills/zan-saas-benefit/references/card-grant-list.md +11 -0
  24. package/skills/zan-saas-benefit/references/card-list.md +7 -0
  25. package/skills/zan-saas-benefit/references/customer-card-get.md +7 -0
  26. package/skills/zan-saas-benefit/references/customer-card-list.md +7 -0
  27. package/skills/zan-saas-coupon/SKILL.md +41 -0
  28. package/skills/zan-saas-coupon/references/activity-get.md +11 -0
  29. package/skills/zan-saas-coupon/references/activity-list.md +15 -0
  30. package/skills/zan-saas-coupon/references/voucher-get.md +13 -0
  31. package/skills/zan-saas-coupon/references/voucher-list.md +25 -0
  32. package/skills/zan-saas-customer/SKILL.md +73 -0
  33. package/skills/zan-saas-customer/references/customer-scan.md +11 -0
  34. package/skills/zan-saas-customer/references/segment-list.md +22 -0
  35. package/skills/zan-saas-fulfillment/SKILL.md +36 -0
  36. package/skills/zan-saas-fulfillment/references/delivery-get.md +11 -0
  37. package/skills/zan-saas-fulfillment/references/shipment-list.md +11 -0
  38. package/skills/zan-saas-fulfillment/references/waybill-get.md +9 -0
  39. package/skills/zan-saas-interaction/SKILL.md +37 -0
  40. package/skills/zan-saas-interaction/references/lottery-get.md +11 -0
  41. package/skills/zan-saas-interaction/references/lottery-list.md +19 -0
  42. package/skills/zan-saas-item/SKILL.md +61 -0
  43. package/skills/zan-saas-item/references/attribute-list.md +11 -0
  44. package/skills/zan-saas-item/references/brand-list.md +7 -0
  45. package/skills/zan-saas-item/references/classification-get.md +7 -0
  46. package/skills/zan-saas-item/references/classification-list.md +7 -0
  47. package/skills/zan-saas-item/references/group-list.md +9 -0
  48. package/skills/zan-saas-item/references/item-content.md +9 -0
  49. package/skills/zan-saas-item/references/item-get.md +9 -0
  50. package/skills/zan-saas-item/references/item-search.md +23 -0
  51. package/skills/zan-saas-item/references/product-identity.md +11 -0
  52. package/skills/zan-saas-member/SKILL.md +41 -0
  53. package/skills/zan-saas-member/references/customer-level-get.md +7 -0
  54. package/skills/zan-saas-member/references/growth-log-list.md +17 -0
  55. package/skills/zan-saas-member/references/membership-get.md +7 -0
  56. package/skills/zan-saas-order/SKILL.md +56 -0
  57. package/skills/zan-saas-order/references/order-get.md +26 -0
  58. package/skills/zan-saas-order/references/order-list.md +25 -0
  59. package/skills/zan-saas-order/references/order-scan.md +35 -0
  60. package/skills/zan-saas-org/SKILL.md +37 -0
  61. package/skills/zan-saas-org/references/staff-get.md +15 -0
  62. package/skills/zan-saas-org/references/staff-role-get.md +11 -0
  63. package/skills/zan-saas-org/references/unit-get.md +11 -0
  64. package/skills/zan-saas-org/references/unit-list.md +15 -0
  65. package/skills/zan-saas-points/SKILL.md +42 -0
  66. package/skills/zan-saas-points/references/account-get.md +7 -0
  67. package/skills/zan-saas-points/references/log-list.md +17 -0
  68. package/skills/zan-saas-points/references/rule-list.md +2 -0
  69. package/skills/zan-saas-privacy/SKILL.md +30 -0
  70. package/skills/zan-saas-privacy/references/field-decrypt.md +19 -0
  71. package/skills/zan-saas-shared/SKILL.md +94 -0
  72. package/skills/zan-saas-shared/references/authentication.md +52 -0
  73. package/skills/zan-saas-shared/references/value-processing.md +27 -0
  74. package/skills/zan-saas-stock/SKILL.md +28 -0
  75. package/skills/zan-saas-stock/references/log-list.md +11 -0
  76. package/skills/zan-saas-valuecard/SKILL.md +37 -0
  77. package/skills/zan-saas-valuecard/references/card-get.md +13 -0
  78. package/skills/zan-saas-valuecard/references/card-list.md +11 -0
  79. package/src/auth/browser.js +28 -0
  80. package/src/auth/callback-server.js +99 -0
  81. package/src/auth/config.js +53 -0
  82. package/src/auth/credential-store.js +109 -0
  83. package/src/auth/oauth-client.js +105 -0
  84. package/src/auth/pkce.js +28 -0
  85. package/src/auth/session.js +178 -0
  86. package/src/auth/tool.js +25 -0
  87. package/src/cli/command-paths.js +126 -0
  88. package/src/cli/context-help.js +102 -0
  89. package/src/cli/diagnostic-command.js +57 -0
  90. package/src/cli/domain-catalog.js +42 -0
  91. package/src/cli/help-text.js +32 -0
  92. package/src/cli/native-loader.js +120 -0
  93. package/src/cli/native-path.js +22 -0
  94. package/src/cli/parser-policy.js +141 -0
  95. package/src/cli/program.js +134 -0
  96. package/src/cli/skill-catalog.js +241 -0
  97. package/src/commands/native/access/commands.index.json +24 -0
  98. package/src/commands/native/access/domain.json +3 -0
  99. package/src/commands/native/access/role/list/command.js +141 -0
  100. package/src/commands/native/access/role/list/schema.json +393 -0
  101. package/src/commands/native/access/staff-perms/get/command.js +119 -0
  102. package/src/commands/native/access/staff-perms/get/schema.json +703 -0
  103. package/src/commands/native/aftersale/case/get/command.js +104 -0
  104. package/src/commands/native/aftersale/case/get/exact-json.js +52 -0
  105. package/src/commands/native/aftersale/case/get/money.js +28 -0
  106. package/src/commands/native/aftersale/case/get/projection.js +120 -0
  107. package/src/commands/native/aftersale/case/get/query.js +72 -0
  108. package/src/commands/native/aftersale/case/get/schema.json +134 -0
  109. package/src/commands/native/aftersale/case/list/command.js +135 -0
  110. package/src/commands/native/aftersale/case/list/schema.json +1095 -0
  111. package/src/commands/native/aftersale/case/scan/command.js +110 -0
  112. package/src/commands/native/aftersale/case/scan/query.js +62 -0
  113. package/src/commands/native/aftersale/case/scan/scan.js +78 -0
  114. package/src/commands/native/aftersale/case/scan/schema.json +964 -0
  115. package/src/commands/native/aftersale/case/scan/search.js +108 -0
  116. package/src/commands/native/aftersale/commands.index.json +42 -0
  117. package/src/commands/native/aftersale/domain.json +3 -0
  118. package/src/commands/native/aftersale/refund/list/command.js +97 -0
  119. package/src/commands/native/aftersale/refund/list/money.js +25 -0
  120. package/src/commands/native/aftersale/refund/list/projection.js +50 -0
  121. package/src/commands/native/aftersale/refund/list/query.js +73 -0
  122. package/src/commands/native/aftersale/refund/list/schema.json +130 -0
  123. package/src/commands/native/aftersale/shared/query.js +58 -0
  124. package/src/commands/native/aftersale/shared/search.js +128 -0
  125. package/src/commands/native/benefit/card-definition/get/command.js +110 -0
  126. package/src/commands/native/benefit/card-definition/get/schema.json +545 -0
  127. package/src/commands/native/benefit/card-definition/list/command.js +106 -0
  128. package/src/commands/native/benefit/card-definition/list/schema.json +690 -0
  129. package/src/commands/native/benefit/card-grant/list/command.js +135 -0
  130. package/src/commands/native/benefit/card-grant/list/schema.json +758 -0
  131. package/src/commands/native/benefit/commands.index.json +51 -0
  132. package/src/commands/native/benefit/customer-card/get/command.js +110 -0
  133. package/src/commands/native/benefit/customer-card/get/schema.json +340 -0
  134. package/src/commands/native/benefit/customer-card/list/command.js +103 -0
  135. package/src/commands/native/benefit/customer-card/list/schema.json +731 -0
  136. package/src/commands/native/benefit/domain.json +3 -0
  137. package/src/commands/native/coupon/activity/get/command.js +183 -0
  138. package/src/commands/native/coupon/activity/get/schema.json +624 -0
  139. package/src/commands/native/coupon/activity/list/command.js +169 -0
  140. package/src/commands/native/coupon/activity/list/schema.json +845 -0
  141. package/src/commands/native/coupon/commands.index.json +42 -0
  142. package/src/commands/native/coupon/domain.json +3 -0
  143. package/src/commands/native/coupon/shared/query.js +15 -0
  144. package/src/commands/native/coupon/voucher/get/command.js +113 -0
  145. package/src/commands/native/coupon/voucher/get/schema.json +147 -0
  146. package/src/commands/native/coupon/voucher/list/command.js +155 -0
  147. package/src/commands/native/coupon/voucher/list/schema.json +772 -0
  148. package/src/commands/native/customer/commands.index.json +96 -0
  149. package/src/commands/native/customer/customer/get/adapter.js +82 -0
  150. package/src/commands/native/customer/customer/get/command.js +106 -0
  151. package/src/commands/native/customer/customer/get/schema.json +958 -0
  152. package/src/commands/native/customer/customer/list/command.js +78 -0
  153. package/src/commands/native/customer/customer/list/input.js +145 -0
  154. package/src/commands/native/customer/customer/list/query.js +103 -0
  155. package/src/commands/native/customer/customer/list/schema.json +969 -0
  156. package/src/commands/native/customer/customer/scan/command.js +80 -0
  157. package/src/commands/native/customer/customer/scan/scan.js +92 -0
  158. package/src/commands/native/customer/customer/scan/schema.json +211 -0
  159. package/src/commands/native/customer/customer-tag/get/command.js +166 -0
  160. package/src/commands/native/customer/customer-tag/get/schema.json +317 -0
  161. package/src/commands/native/customer/customer-tag/list/command.js +143 -0
  162. package/src/commands/native/customer/customer-tag/list/schema.json +259 -0
  163. package/src/commands/native/customer/domain.json +3 -0
  164. package/src/commands/native/customer/segment/list/command.js +96 -0
  165. package/src/commands/native/customer/segment/list/query.js +98 -0
  166. package/src/commands/native/customer/segment/list/schema.json +135 -0
  167. package/src/commands/native/customer/segment-customer/list/command.js +171 -0
  168. package/src/commands/native/customer/segment-customer/list/schema.json +123 -0
  169. package/src/commands/native/customer/shared/search-input.js +179 -0
  170. package/src/commands/native/customer/shared/search-page.js +54 -0
  171. package/src/commands/native/customer/tag/get/command.js +73 -0
  172. package/src/commands/native/customer/tag/get/schema.json +339 -0
  173. package/src/commands/native/customer/tagcategory/get/command.js +80 -0
  174. package/src/commands/native/customer/tagcategory/get/query.js +98 -0
  175. package/src/commands/native/customer/tagcategory/get/schema.json +339 -0
  176. package/src/commands/native/customer/tagcategory/list/command.js +127 -0
  177. package/src/commands/native/customer/tagcategory/list/schema.json +394 -0
  178. package/src/commands/native/fulfillment/commands.index.json +33 -0
  179. package/src/commands/native/fulfillment/delivery/get/command.js +107 -0
  180. package/src/commands/native/fulfillment/delivery/get/projection.js +142 -0
  181. package/src/commands/native/fulfillment/delivery/get/schema.json +173 -0
  182. package/src/commands/native/fulfillment/domain.json +3 -0
  183. package/src/commands/native/fulfillment/shipment/list/command.js +83 -0
  184. package/src/commands/native/fulfillment/shipment/list/projection.js +130 -0
  185. package/src/commands/native/fulfillment/shipment/list/schema.json +752 -0
  186. package/src/commands/native/fulfillment/waybill/get/command.js +193 -0
  187. package/src/commands/native/fulfillment/waybill/get/response.js +68 -0
  188. package/src/commands/native/fulfillment/waybill/get/schema.json +190 -0
  189. package/src/commands/native/interaction/commands.index.json +24 -0
  190. package/src/commands/native/interaction/domain.json +3 -0
  191. package/src/commands/native/interaction/lottery/get/command.js +61 -0
  192. package/src/commands/native/interaction/lottery/get/query.js +152 -0
  193. package/src/commands/native/interaction/lottery/get/schema.json +1004 -0
  194. package/src/commands/native/interaction/lottery/list/command.js +176 -0
  195. package/src/commands/native/interaction/lottery/list/schema.json +898 -0
  196. package/src/commands/native/item/attribute/list/command.js +122 -0
  197. package/src/commands/native/item/attribute/list/schema.json +727 -0
  198. package/src/commands/native/item/brand/list/command.js +114 -0
  199. package/src/commands/native/item/brand/list/schema.json +725 -0
  200. package/src/commands/native/item/classification/get/command.js +87 -0
  201. package/src/commands/native/item/classification/get/schema.json +596 -0
  202. package/src/commands/native/item/classification/list/command.js +105 -0
  203. package/src/commands/native/item/classification/list/schema.json +676 -0
  204. package/src/commands/native/item/commands.index.json +87 -0
  205. package/src/commands/native/item/domain.json +1 -0
  206. package/src/commands/native/item/group/list/command.js +122 -0
  207. package/src/commands/native/item/group/list/schema.json +809 -0
  208. package/src/commands/native/item/item/get/command.js +122 -0
  209. package/src/commands/native/item/item/get/schema.json +3799 -0
  210. package/src/commands/native/item/item/list/command.js +7 -0
  211. package/src/commands/native/item/item/list/schema.json +1118 -0
  212. package/src/commands/native/item/item/scan/command.js +7 -0
  213. package/src/commands/native/item/item/scan/schema.json +1046 -0
  214. package/src/commands/native/item/item-content/get/command.js +54 -0
  215. package/src/commands/native/item/item-content/get/schema.json +677 -0
  216. package/src/commands/native/item/shared/api.js +31 -0
  217. package/src/commands/native/item/shared/content.js +20 -0
  218. package/src/commands/native/item/shared/diagnostics.js +61 -0
  219. package/src/commands/native/item/shared/h5.js +63 -0
  220. package/src/commands/native/item/shared/identity.js +62 -0
  221. package/src/commands/native/item/shared/normalize.js +49 -0
  222. package/src/commands/native/item/shared/scope.js +22 -0
  223. package/src/commands/native/item/shared/search-command.js +61 -0
  224. package/src/commands/native/item/shared/search-options.js +150 -0
  225. package/src/commands/native/item/shared/search.js +158 -0
  226. package/src/commands/native/member/commands.index.json +60 -0
  227. package/src/commands/native/member/customer-level/get/command.js +122 -0
  228. package/src/commands/native/member/customer-level/get/schema.json +300 -0
  229. package/src/commands/native/member/domain.json +3 -0
  230. package/src/commands/native/member/growth/get/command.js +64 -0
  231. package/src/commands/native/member/growth/get/schema.json +283 -0
  232. package/src/commands/native/member/growth-log/list/command.js +203 -0
  233. package/src/commands/native/member/growth-log/list/schema.json +695 -0
  234. package/src/commands/native/member/level/get/command.js +70 -0
  235. package/src/commands/native/member/level/get/query.js +125 -0
  236. package/src/commands/native/member/level/get/schema.json +339 -0
  237. package/src/commands/native/member/level/list/command.js +50 -0
  238. package/src/commands/native/member/level/list/query.js +109 -0
  239. package/src/commands/native/member/level/list/schema.json +104 -0
  240. package/src/commands/native/member/membership/get/command.js +78 -0
  241. package/src/commands/native/member/membership/get/schema.json +283 -0
  242. package/src/commands/native/order/commands.index.json +33 -0
  243. package/src/commands/native/order/domain.json +3 -0
  244. package/src/commands/native/order/order/get/command.js +155 -0
  245. package/src/commands/native/order/order/get/schema.json +1167 -0
  246. package/src/commands/native/order/order/list/command.js +230 -0
  247. package/src/commands/native/order/order/list/schema.json +1363 -0
  248. package/src/commands/native/order/order/scan/command.js +148 -0
  249. package/src/commands/native/order/order/scan/scan.js +89 -0
  250. package/src/commands/native/order/order/scan/schema.json +1445 -0
  251. package/src/commands/native/order/shared/projection.js +183 -0
  252. package/src/commands/native/order/shared/query.js +84 -0
  253. package/src/commands/native/order/shared/search.js +112 -0
  254. package/src/commands/native/org/commands.index.json +42 -0
  255. package/src/commands/native/org/domain.json +3 -0
  256. package/src/commands/native/org/shared/single-staff.js +115 -0
  257. package/src/commands/native/org/staff/get/command.js +148 -0
  258. package/src/commands/native/org/staff/get/schema.json +114 -0
  259. package/src/commands/native/org/staff-role/get/command.js +45 -0
  260. package/src/commands/native/org/staff-role/get/schema.json +93 -0
  261. package/src/commands/native/org/unit/get/command.js +131 -0
  262. package/src/commands/native/org/unit/get/schema.json +478 -0
  263. package/src/commands/native/org/unit/list/command.js +187 -0
  264. package/src/commands/native/org/unit/list/schema.json +828 -0
  265. package/src/commands/native/points/account/get/command.js +65 -0
  266. package/src/commands/native/points/account/get/schema.json +303 -0
  267. package/src/commands/native/points/commands.index.json +33 -0
  268. package/src/commands/native/points/domain.json +3 -0
  269. package/src/commands/native/points/log/list/command.js +141 -0
  270. package/src/commands/native/points/log/list/schema.json +761 -0
  271. package/src/commands/native/points/rule/list/command.js +82 -0
  272. package/src/commands/native/points/rule/list/schema.json +294 -0
  273. package/src/commands/native/privacy/commands.index.json +15 -0
  274. package/src/commands/native/privacy/domain.json +3 -0
  275. package/src/commands/native/privacy/field/decrypt/command.js +185 -0
  276. package/src/commands/native/privacy/field/decrypt/schema.json +656 -0
  277. package/src/commands/native/privacy/shared/order-source.js +33 -0
  278. package/src/commands/native/stock/commands.index.json +15 -0
  279. package/src/commands/native/stock/domain.json +3 -0
  280. package/src/commands/native/stock/log/list/command.js +207 -0
  281. package/src/commands/native/stock/log/list/schema.json +922 -0
  282. package/src/commands/native/valuecard/card/get/command.js +107 -0
  283. package/src/commands/native/valuecard/card/get/schema.json +683 -0
  284. package/src/commands/native/valuecard/card/list/command.js +136 -0
  285. package/src/commands/native/valuecard/card/list/pagination.js +23 -0
  286. package/src/commands/native/valuecard/card/list/schema.json +780 -0
  287. package/src/commands/native/valuecard/commands.index.json +24 -0
  288. package/src/commands/native/valuecard/domain.json +3 -0
  289. package/src/commands/native/valuecard/shared/projection.js +57 -0
  290. package/src/commands/system/api.js +47 -0
  291. package/src/commands/system/auth.js +118 -0
  292. package/src/commands/system/contracts.js +373 -0
  293. package/src/commands/system/doctor.js +24 -0
  294. package/src/commands/system/install.js +41 -0
  295. package/src/commands/system/printer.js +16 -0
  296. package/src/commands/system/schema.js +162 -0
  297. package/src/commands/system/shop-guidance.js +9 -0
  298. package/src/commands/system/shop.js +237 -0
  299. package/src/commands/system/skills.js +49 -0
  300. package/src/execution/beijing-date-time.js +29 -0
  301. package/src/execution/beijing-time.js +21 -0
  302. package/src/execution/bounded-input.js +42 -0
  303. package/src/execution/command-io.js +137 -0
  304. package/src/execution/command-policy.js +41 -0
  305. package/src/execution/command-schema.js +131 -0
  306. package/src/execution/datetime.js +83 -0
  307. package/src/execution/error-details.js +399 -0
  308. package/src/execution/error-model.js +82 -0
  309. package/src/execution/exact-json.js +80 -0
  310. package/src/execution/list-total.js +10 -0
  311. package/src/execution/money.js +59 -0
  312. package/src/execution/parser-error.js +66 -0
  313. package/src/execution/privacy.js +197 -0
  314. package/src/execution/public-schema.js +52 -0
  315. package/src/execution/query-api.js +101 -0
  316. package/src/execution/range.js +84 -0
  317. package/src/execution/request-state.js +55 -0
  318. package/src/execution/schema-completeness.js +116 -0
  319. package/src/execution/shop-query.js +54 -0
  320. package/src/execution/target-shop-default.js +23 -0
  321. package/src/execution/target-shop.js +49 -0
  322. package/src/identity/context-store.js +54 -0
  323. package/src/identity/context.js +38 -0
  324. package/src/installation/install.js +158 -0
  325. package/src/installation/npm-error.js +93 -0
  326. package/src/installation/skill.js +113 -0
  327. package/src/transport/account-api.js +152 -0
  328. package/src/transport/api.js +199 -0
  329. package/src/transport/shop-app.js +63 -0
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ const { readApi, ItemQueryError, object } = require('./api');
4
+ const CONTENT_API = 'youzan.item.itemdesc.get/1.0.0';
5
+
6
+ function contentRequest(scope) {
7
+ return { method: 'POST', path: CONTENT_API,
8
+ body: { request: { kdt_id: scope.kdtId, item_id: scope.item_id, channel: scope.channel } } };
9
+ }
10
+
11
+ async function readContent(scope, command = 'item.item.get') {
12
+ const request = contentRequest(scope);
13
+ const content = await readApi(request.path, request.body, 'content', scope, command);
14
+ if (!object(content)) {
15
+ throw new ItemQueryError('protocol', 'content', scope, { api: CONTENT_API }, 'sent', command);
16
+ }
17
+ return content;
18
+ }
19
+
20
+ module.exports = { CONTENT_API, contentRequest, readContent };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const { parseIntegerOption, inputIssue, constraints, modelAt } = require('../../../../execution/error-details');
4
+ const { normalizeMoney } = require('../../../../execution/money');
5
+ const { normalizeTimes } = require('../../../../execution/datetime');
6
+ const { validateCommandValue } = require('../../../../execution/command-schema');
7
+
8
+ function integer(raw, schema, path) {
9
+ return parseIntegerOption(typeof raw === 'string' ? raw.trim() : raw, schema, path);
10
+ }
11
+ function itemScope(options, schema) {
12
+ const scope = { kdtId: integer(options.kdtId, schema, '/kdtId'),
13
+ item_id: integer(options.itemId, schema, '/item_id'), channel: Number(options.channel) };
14
+ if (!['0', '1'].includes(options.channel)) {
15
+ throw inputIssue('/channel', 'invalid_choice', constraints(modelAt(schema, 'input', '/channel')));
16
+ }
17
+ validateCommandValue(schema, 'input', scope);
18
+ return scope;
19
+ }
20
+ function outputIssue(failure, schema, path, reason = 'constraint') {
21
+ const error = failure();
22
+ error.issues = [{ source: 'output', path, reason,
23
+ expected: constraints(modelAt(schema, 'output', path)) }];
24
+ return error;
25
+ }
26
+ // Traverse only declared fields. Paths contain static model names and array indexes, never data keys.
27
+ function normalize(value, schema, path, failure) {
28
+ const model = modelAt(schema, 'output', path);
29
+ if (!model || value == null) return value;
30
+ if (model.$comment?.startsWith('upstream-money:') || model.$comment?.startsWith('upstream-time:')) {
31
+ try { return normalizeTimes(normalizeMoney(value, model), model, schema.definitions); }
32
+ catch { throw outputIssue(failure, schema, path, 'invalid_format'); }
33
+ }
34
+ if (Array.isArray(value) && model.items) {
35
+ return value.map((entry, index) => normalize(entry, schema, `${path}/${index}`, failure));
36
+ }
37
+ if (typeof value === 'object' && !Array.isArray(value) && model.properties) {
38
+ const result = { ...value };
39
+ for (const key of Object.keys(model.properties)) {
40
+ if (Object.hasOwn(value, key)) result[key] = normalize(value[key], schema, `${path}/${key}`, failure);
41
+ }
42
+ return result;
43
+ }
44
+ return value;
45
+ }
46
+ function validateOutput(schema, result, failure) {
47
+ try { validateCommandValue(schema, 'output', result); }
48
+ catch (cause) {
49
+ const error = failure();
50
+ if (cause.issues) error.issues = cause.issues;
51
+ if (cause.issuesTruncated) error.issuesTruncated = true;
52
+ throw error;
53
+ }
54
+ }
55
+ function pagination(pageNo, returned, hasMore, total) {
56
+ return { pageNo, pageSize: 20, returned, hasMore,
57
+ ...(total === null ? {} : { total }),
58
+ nextPageNo: hasMore === true && pageNo < 2147483647 ? pageNo + 1 : null,
59
+ windowLimitReached: hasMore !== false && pageNo === 2147483647 };
60
+ }
61
+ module.exports = { integer, itemScope, outputIssue, normalize, validateOutput, pagination };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const { ItemQueryError, readApi, object } = require('./api');
4
+ const SHOP_API = 'youzan.shop.basic.get/3.0.0';
5
+ const ORG_API = 'youzan.shop.chain.descendent.organization.list/1.0.1';
6
+ const PAGE_SIZE = 50;
7
+ const MAX_PAGES = 100;
8
+ const knownRoles = new Set([0, 1, 2001, 2002, 3, 4, 6, 7]);
9
+
10
+ async function resolveRole(scope, command) {
11
+ const failure = (category, stage, facts = {}) =>
12
+ new ItemQueryError(category, stage, scope, facts, 'sent', command);
13
+ const basic = await readApi(SHOP_API, {}, 'shop-role', scope, command);
14
+ if (!object(basic) || !knownRoles.has(basic.role) ||
15
+ !Number.isSafeInteger(basic.sid ?? basic.kdt_id)) {
16
+ throw failure('protocol', 'shop-role', { api: SHOP_API });
17
+ }
18
+ if ((basic.sid ?? basic.kdt_id) === scope.kdtId) return basic.role;
19
+ if (basic.role !== 1) {
20
+ throw failure('scope_unresolved', 'shop-role');
21
+ }
22
+ const seen = new Set();
23
+ let read = 0;
24
+ for (let page = 1; page <= MAX_PAGES; page++) {
25
+ const data = await readApi(ORG_API, { page_num: page, page_size: PAGE_SIZE }, 'organization-role', scope, command);
26
+ if (!object(data) || !Array.isArray(data.organization_list)) {
27
+ throw failure('protocol', 'organization-role', { api: ORG_API });
28
+ }
29
+ const total = data.total ?? data.paginator?.total_count;
30
+ if (total != null && (!Number.isSafeInteger(total) || total < 0)) {
31
+ throw failure('protocol', 'organization-role', { api: ORG_API });
32
+ }
33
+ let added = 0;
34
+ for (const org of data.organization_list) {
35
+ if (!object(org) || !Number.isSafeInteger(org.kdt_id) || !knownRoles.has(org.role)) {
36
+ throw failure('protocol', 'organization-role', { api: ORG_API });
37
+ }
38
+ if (org.kdt_id === scope.kdtId) return org.role;
39
+ if (!seen.has(org.kdt_id)) { seen.add(org.kdt_id); added++; }
40
+ }
41
+ read += data.organization_list.length;
42
+ if (data.organization_list.length < PAGE_SIZE || (total != null && read >= total)) break;
43
+ if (added === 0) throw failure('pagination_stalled', 'organization-role');
44
+ if (page === MAX_PAGES) throw failure('budget_exceeded', 'organization-role');
45
+ }
46
+ throw failure('scope_unresolved', 'organization-role');
47
+ }
48
+ async function attachH5(item, scope) {
49
+ await attachH5Items([item], scope);
50
+ }
51
+ async function attachH5Items(items, scope, command = 'item.item.get') {
52
+ if (items.length === 0) return;
53
+ const role = await resolveRole(scope, command);
54
+ const applicable = role === 0 || (role === 2002 && scope.channel === 0);
55
+ for (const item of items) {
56
+ if (!applicable) { item.h5_url = null; continue; }
57
+ if (typeof item.alias !== 'string' || !item.alias.trim()) {
58
+ throw new ItemQueryError('missing_alias', 'h5-link', scope, {}, 'sent', command);
59
+ }
60
+ item.h5_url = `https://h5.youzan.com/v2/goods/${item.alias.trim()}`;
61
+ }
62
+ }
63
+ module.exports = { attachH5, attachH5Items, SHOP_API, ORG_API, PAGE_SIZE, MAX_PAGES };
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const detailPath = 'youzan.item.itemdetail.get/1.0.0';
4
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
5
+
6
+ function mappingRequest(scope) {
7
+ return { method: 'POST', path: detailPath, body: { request: {
8
+ kdt_id: Number(scope.kdtId), item_id: Number(scope.item_id), channel: scope.channel
9
+ } } };
10
+ }
11
+
12
+ function channelIdentity(source, scope, { skuMapping = false } = {}) {
13
+ if (!object(source) || source.item_id !== Number(scope.item_id) ||
14
+ !Number.isSafeInteger(source.channel_item_id) || source.channel_item_id < 1 ||
15
+ (source.kdt_id != null && source.kdt_id !== Number(scope.kdtId)) ||
16
+ (source.channel != null && source.channel !== scope.channel)) {
17
+ throw new Error('Product identity mismatch');
18
+ }
19
+ const skus = new Map();
20
+ const unified = new Set();
21
+ if (skuMapping && source.skus != null) {
22
+ if (!Array.isArray(source.skus)) throw new Error('Invalid SKU identities');
23
+ for (const sku of source.skus) {
24
+ if (!object(sku)) throw new Error('Invalid SKU identity');
25
+ if (sku.sku_id == null || sku.channel_sku_id == null) continue;
26
+ if (!Number.isSafeInteger(sku.sku_id) || sku.sku_id < 0 ||
27
+ !Number.isSafeInteger(sku.channel_sku_id) || sku.channel_sku_id < 0 ||
28
+ skus.has(sku.channel_sku_id) || unified.has(sku.sku_id)) {
29
+ throw new Error('Ambiguous SKU identities');
30
+ }
31
+ skus.set(sku.channel_sku_id, sku.sku_id);
32
+ unified.add(sku.sku_id);
33
+ }
34
+ }
35
+ return { item_id: source.item_id, channel_item_id: source.channel_item_id, skus };
36
+ }
37
+
38
+ function legacyLog(source, identity) {
39
+ if (!object(source) || source.item_id !== identity.channel_item_id ||
40
+ (Object.hasOwn(source, 'unified_item_id') && source.unified_item_id !== identity.item_id)) {
41
+ throw new Error('Log identity mismatch');
42
+ }
43
+ const { item_id, sku_id, unified_item_id, unified_sku_id, channel_item_id, channel_sku_id, ...rest } = source;
44
+ if ((channel_item_id !== undefined && channel_item_id !== item_id) ||
45
+ (channel_sku_id !== undefined && channel_sku_id !== sku_id)) throw new Error('Conflicting log identities');
46
+ const output = { ...rest, item_id: identity.item_id, channel_item_id: item_id };
47
+ if (Object.hasOwn(source, 'sku_id')) {
48
+ if (!Number.isSafeInteger(sku_id)) throw new Error('Invalid log SKU');
49
+ output.channel_sku_id = sku_id;
50
+ if (identity.skus.has(sku_id)) output.sku_id = identity.skus.get(sku_id);
51
+ }
52
+ if (Object.hasOwn(source, 'unified_sku_id')) {
53
+ if (!Number.isSafeInteger(unified_sku_id) || unified_sku_id < 0 ||
54
+ (output.sku_id !== undefined && output.sku_id !== unified_sku_id)) {
55
+ throw new Error('Conflicting unified SKU');
56
+ }
57
+ output.sku_id = unified_sku_id;
58
+ }
59
+ return output;
60
+ }
61
+
62
+ module.exports = { detailPath, mappingRequest, channelIdentity, legacyLog };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const { object } = require('./api');
4
+
5
+ // Inventory quantities belong to independent inventory queries, including nested combo SKUs.
6
+ const inventoryFields = new Set([
7
+ 'stock_num', 'stock_num_str', 'spot_stock_num', 'spot_stock_num_str',
8
+ 'plan_stock_num', 'plan_stock_num_str', 'total_stock', 'total_stock_str',
9
+ 'stock_display_note', 'stockNum', 'stockNumStr', 'spotStockNum', 'spotStockNumStr',
10
+ 'planStockNum', 'planStockNumStr', 'totalStock', 'totalStockStr', 'stockDisplayNote',
11
+ 'stock_count', 'stock_quantity', 'available_stock', 'available_stock_num',
12
+ 'freeze_stock', 'freeze_stock_num', 'frozen_stock', 'frozen_stock_num',
13
+ 'stockCount', 'stockQuantity', 'availableStock', 'availableStockNum',
14
+ 'freezeStock', 'freezeStockNum', 'frozenStock', 'frozenStockNum'
15
+ ]);
16
+ function withoutInventory(value) {
17
+ if (Array.isArray(value)) return value.map(withoutInventory);
18
+ if (!object(value)) return value;
19
+ return Object.fromEntries(Object.entries(value)
20
+ .filter(([key]) => !inventoryFields.has(key)).map(([key, child]) => [key, withoutInventory(child)]));
21
+ }
22
+ function firstText(values) {
23
+ for (const value of values) {
24
+ if (value != null && String(value).trim()) return String(value).trim();
25
+ }
26
+ }
27
+ function hasSpecs(list) {
28
+ return Array.isArray(list) && list.some(entry => object(entry) &&
29
+ ((Array.isArray(entry.spec_values) && entry.spec_values.length > 0) ||
30
+ firstText([entry.spec_name, entry.spec_value_name, entry.name, entry.value]) != null));
31
+ }
32
+ function normalizeItem(raw) {
33
+ const item = withoutInventory(raw);
34
+ if (item.item_code == null) {
35
+ const code = firstText([item.item_no, item.outer_item_code, item.outer_item_id, item.goods_code, item.code]);
36
+ if (code !== undefined) item.item_code = code;
37
+ }
38
+ const skus = Array.isArray(item.skus) ? item.skus : [];
39
+ for (const sku of skus) {
40
+ if (!object(sku) || sku.sku_code != null) continue;
41
+ const code = firstText([sku.sku_no, sku.outer_sku_code, sku.outer_sku_id, sku.code]);
42
+ if (code !== undefined) sku.sku_code = code;
43
+ }
44
+ item.is_multi_sku = skus.length > 1 || hasSpecs(item.specs) || skus.some(sku => hasSpecs([sku]));
45
+ item.is_multi_unit = object(item.ability_mark?.item_mark?.multi_unit_mark);
46
+ return item;
47
+ }
48
+
49
+ module.exports = { normalizeItem, withoutInventory, inventoryFields };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ function positiveId(raw, flag) {
4
+ const value = typeof raw === 'string' ? raw.trim() : '';
5
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
6
+ throw new Error(`${flag} 须为正整数且不超过 9007199254740991`);
7
+ }
8
+ return Number(value);
9
+ }
10
+
11
+ function itemScope(options) {
12
+ const scope = {
13
+ kdtId: positiveId(options.kdtId, '--kdt-id'),
14
+ item_id: positiveId(options.itemId, '--item-id'),
15
+ channel: Number(options.channel)
16
+ };
17
+ if (!['0', '1'].includes(options.channel)) throw new Error('--channel 仅支持 0 或 1');
18
+ if (!['json', 'pretty'].includes(options.format)) throw new Error('--format 仅支持 json 或 pretty');
19
+ return scope;
20
+ }
21
+
22
+ module.exports = { itemScope, positiveId };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const { DiagnosticCommand } = require('../../../../cli/diagnostic-command');
4
+ const { ItemQueryError } = require('./api');
5
+ const { buildInput, apiInput, addFilters } = require('./search-options');
6
+ const { execute, preview } = require('./search');
7
+ const { validateCommandValue } = require('../../../../execution/command-schema');
8
+ const { resolveTargetShop } = require('../../../../execution/target-shop');
9
+ function createSearchCommand(mode, schema, context) {
10
+ const path = `item.item.${mode}`;
11
+ const parameter = () => {
12
+ const failure = new ItemQueryError('parameter', 'input', {}, {}, 'not_sent', path);
13
+ failure.message = `商品搜索参数无效,请查看 zan-saas item item ${mode} --help。`;
14
+ return failure;
15
+ };
16
+ let command = addFilters(new DiagnosticCommand(mode, { path, schema, parameterCategory: 'parameter', optionPaths: { '/item_ids': 'itemIds' } }), mode)
17
+ .description(mode === 'list' ? '查询指定店铺和渠道的一页商品' : '按创建时间分片遍历商品;预算不足时整体失败,不返回部分结果')
18
+ .allowExcessArguments(false);
19
+
20
+ if (mode === 'list') command = command.option('--page-no <n>', '页码,从 1 开始;page-no × page-size ≤ 5000', '1')
21
+ .option('--page-size <n>', '每页 1 至 50 条', '20')
22
+ .option('--sorts <values>', '官方排序枚举列表,如 ACTUAL_SOLD_NUM_DESC;最多20项');
23
+ else command = command.option('--max-items <n>', '去重结果预算,1 至 50000;超限整体失败', '1000')
24
+ .option('--max-requests <n>', '搜索与探测预算,1 至 2000;不含后续 H5 角色和组织查询', '100');
25
+ return command.addHelpText('after', `
26
+ 默认补适用 H5,不返回库存数量;不根据缺失 SKU 推断规格标记。
27
+ 筛选和返回的 item_id 是分店、渠道间统一的商品 ID,channel_item_id 是目标店铺渠道的商品 ID。
28
+ 需要商品详情或图文时,继续使用 item_id 和相同店铺、渠道,不把 channel_item_id 填入 --item-id。
29
+ 已定义金额统一为元、固定两位小数字符串;price从分精确转换,origin_price仅规范小数位,原空串保留。
30
+ 不提供 group/content 扩展;需要关联详情时使用已发布的 item item get。
31
+ ${mode === 'list' ? '只读取当前页;匹配总数见 pagination.total,未提供时为null。nextPageNo非null时保留筛选和pageSize继续;windowLimitReached=true时缩小筛选,或明确完整创建时间窗口后用scan。' :
32
+ '固定50/页、创建时间与ID降序;必须显式提供完整创建窗口且开始早于结束,时间闭区间二分,其他筛选始终保留。\n成功返回 data.items 与 data.scope,表示本次声明范围的扫描已完成,不保证并发更新时的快照全集。超预算不输出部分商品;缩小时间/筛选或显式调整有界预算后重新查询。'}
33
+ 搜索 JSON 数据累计最多10MiB,完整输出最多5MiB;角色查询另至多1+100次,找到目标即停。
34
+ 时间输入统一为北京时间 YYYY-MM-DD HH:mm:ss,可追加 1 至 3 位毫秒;不接受时间戳、T 或时区后缀。
35
+ 区间使用 --created-at/--updated-at "START..END",必须包含 ..;省略边界不会补值。
36
+ 已定义时间点输出为 YYYY-MM-DD HH:mm:ss.SSS;请求预览展示接口使用的毫秒整数。
37
+ 必需查询失败时 stdout 为空、stderr 为错误 JSON;无自动重试,不切换目标。
38
+ 执行需要有效的用户、组织与店铺上下文,可通过登录后选店或 ZAN_SAAS_CONTEXT 提供;目标范围和服务端授权按本命令契约校验,公共调用前置见 zan-saas skills read zan-saas-shared。
39
+ at: all 允许总部和非总部位置;目标店铺可显式指定或取当前上下文,由服务端校验用户和店铺权限。
40
+ 缺少上下文时可本地 --dry-run;提供了无效上下文时拒绝预览和真实调用。
41
+
42
+ 示例:
43
+ zan-saas item item ${mode} --kdt-id 101919147 --item-ids 5399188439 ${mode === 'scan' ? '--created-at "2026-10-01 00:00:00..2026-11-01 00:00:00" ' : ''}--dry-run
44
+ zan-saas item item ${mode} --kdt-id 101919147 ${mode === 'list' ? '--page-size 10 --sorts ACTUAL_SOLD_NUM_DESC' : '--created-at "2026-10-01 00:00:00..2026-11-01 00:00:00" --max-items 1000 --max-requests 100'}
45
+ `).action(async options => {
46
+ let input;
47
+ try {
48
+ const kdtId = Object.hasOwn(options, 'kdtId') ? options.kdtId :
49
+ String(resolveTargetShop(options, context, path).kdtId);
50
+ input = buildInput({ ...options, kdtId }, mode, schema);
51
+ validateCommandValue(schema, 'input', input);
52
+ input = apiInput(input, mode);
53
+ }
54
+ catch (error) { if (error.issues || error.cliDetails) throw error; throw parameter(); }
55
+ let result;
56
+ if (options.dryRun) { result = preview(input, mode); validateCommandValue(schema, 'preview', result); }
57
+ else result = await execute(input, mode, schema);
58
+ process.stdout.write(`${JSON.stringify(result, null, options.format === 'pretty' ? 2 : 0)}\n`);
59
+ });
60
+ }
61
+ module.exports = { createSearchCommand };
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const { Option } = require('commander');
4
+ const { parseDateTime } = require('../../../../execution/datetime');
5
+ const { parseDateTimeRange } = require('../../../../execution/range');
6
+ const { inputIssue, modelAt, constraints } = require('../../../../execution/error-details');
7
+ const API = 'youzan.item.base.search/1.0.0';
8
+ const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
9
+ const MAX_OUTPUT_BYTES = 5 * 1024 * 1024;
10
+ const sortEnums = ['CREATED_TIME_DESC', 'CREATED_TIME_ASC', 'PRICE_DESC', 'PRICE_ASC',
11
+ 'STOCK_NUM_DESC', 'STOCK_NUM_ASC', 'ID_DESC', 'ID_ASC', 'ACTUAL_SOLD_NUM_DESC', 'ACTUAL_SOLD_NUM_ASC'];
12
+ const typeCodes = {
13
+ 实物商品: '00000000000000000', 蛋糕烘焙: '01000000000100000', 生鲜果蔬: '01000000004204200',
14
+ 海淘商品: '01000000004304300', 虚拟商品: '00000000000000002', 电子卡券: '00000000000000003',
15
+ 付费优惠券: '01000000005100002', 酒店商品: '00000000003503500',
16
+ 付费会员卡: '00000000002002002', 权益卡: '00000000002002002', 服务商品: '10000000000000002'
17
+ };
18
+ const filterFields = [
19
+ ['classificationIds', 'classification_ids', 1], ['item_ids', 'item_ids', 1],
20
+ ['groupIds', 'group_ids', 1], ['secondGroupIds', 'second_group_ids', 1],
21
+ ['notExcludeAbilityCodes', 'not_exclude_ability_codes', 0, 2147483647],
22
+ ['isDisplays', 'is_displays', 0, 1], ['soldStatusList', 'sold_status_list', 0, 3]
23
+ ];
24
+ function integer(raw, name, minimum = 1, maximum = Number.MAX_SAFE_INTEGER) {
25
+ if ((typeof raw !== 'string' && typeof raw !== 'number') || !/^-?\d+$/.test(String(raw).trim())) {
26
+ throw new Error(`${name} 须为范围内的整数,详见 help/schema`);
27
+ }
28
+ const value = Number(raw);
29
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
30
+ throw new Error(`${name} 须为范围内的整数,详见 help/schema`);
31
+ }
32
+ return value;
33
+ }
34
+ function list(raw, name, parse) {
35
+ let values;
36
+ try {
37
+ const text = raw.trim();
38
+ if (!text) throw new Error();
39
+ values = text.startsWith('[') ? JSON.parse(text) : text.split(/[,\s]+/);
40
+ if (!Array.isArray(values) || !values.length || values.length > 20) throw new Error();
41
+ values = values.map(parse);
42
+ } catch { throw new Error(`${name} 须为 1 至 20 项的有效列表,详见 help/schema`); }
43
+ return [...new Set(values)];
44
+ }
45
+ function buildInput(options, mode, schema = require(`../item/${mode}/schema.json`)) {
46
+ function field(name, action) {
47
+ try { return action(); }
48
+ catch (error) {
49
+ const path = `/${name}`;
50
+ const model = modelAt(schema, 'input', path);
51
+ const expected = constraints(model);
52
+ if (model?.items?.enum) expected.items = { enum: model.items.enum };
53
+ if (name === 'createdAt' || name === 'updatedAt') {
54
+ expected.format = '北京时间 YYYY-MM-DD HH:mm:ss[.SSS],区间必须包含 ..';
55
+ }
56
+ throw inputIssue(path, expected.enum || expected.items?.enum ? 'invalid_choice' :
57
+ expected.type === 'integer' && /^-?\d+$/.test(String(options[name])) ? 'out_of_range' : 'invalid_format', expected);
58
+ }
59
+ }
60
+ const input = { kdtId: field('kdtId', () => integer(options.kdtId, '--kdt-id')), channel: field('channel', () => integer(options.channel, '--channel', 0, 1)) };
61
+ if (!['json', 'pretty'].includes(options.format)) throw new Error('--format 仅支持 json 或 pretty');
62
+ if (mode === 'list') {
63
+ input.pageNo = field('pageNo', () => integer(options.pageNo, '--page-no'));
64
+ input.pageSize = field('pageSize', () => integer(options.pageSize, '--page-size', 1, 50));
65
+ if (input.pageNo * input.pageSize > 5000) throw inputIssue('/pageNo', 'constraint', { maximum: Math.floor(5000 / input.pageSize),
66
+ format: '页码 × 每页条数不能超过 5000;请缩小筛选范围。需要跨页扫描时,使用 item item scan 并明确完整创建时间范围。' }, ['/pageSize']);
67
+ } else {
68
+ input.maxItems = field('maxItems', () => integer(options.maxItems, '--max-items', 1, 50000));
69
+ input.maxRequests = field('maxRequests', () => integer(options.maxRequests, '--max-requests', 1, 2000));
70
+ }
71
+ for (const [option, key, min, max] of filterFields) {
72
+ const value = options[option === 'item_ids' ? 'itemIds' : option];
73
+ if (value !== undefined) input[option] = field(option, () => list(value, `--${key.replaceAll('_', '-')}`,
74
+ value => integer(value, key, min, max)));
75
+ }
76
+ if (options.keyword !== undefined) {
77
+ if (!options.keyword.trim()) throw inputIssue('/keyword', 'invalid_format', {type: 'string', minLength: 1});
78
+ input.keyword = options.keyword;
79
+ }
80
+ if (options.itemType !== undefined) {
81
+ const value = typeCodes[options.itemType.trim()] ?? options.itemType.trim();
82
+ if (!/^\d{17}$/.test(value) || ['10000000000000003', '10000000000000004'].includes(value)) {
83
+ throw inputIssue('/itemType', 'invalid_format', { type: 'string', pattern: '^\\d{17}$' });
84
+ }
85
+ input.itemType = value;
86
+ }
87
+ if (options.sorts !== undefined) input.sorts = field('sorts', () => list(options.sorts, '--sorts', value => {
88
+ if (typeof value !== 'string' || !sortEnums.includes(value)) throw new Error();
89
+ return value;
90
+ }));
91
+ for (const option of ['createdAt', 'updatedAt']) {
92
+ if (options[option] !== undefined) {
93
+ const range = field(option, () => parseDateTimeRange(options[option], { precision: 'milliseconds',
94
+ requireBoth: mode === 'scan' && option === 'createdAt',
95
+ strictOrder: mode === 'scan' && option === 'createdAt' }));
96
+ if (Object.values(range).some(value => parseDateTime(value) < 0)) {
97
+ throw inputIssue(`/${option}`, 'out_of_range', { format: '北京时间,不早于 1970-01-01 08:00:00' });
98
+ }
99
+ input[option] = range;
100
+ }
101
+ }
102
+ if (mode === 'scan' && !input.createdAt) throw inputIssue('/createdAt', 'required');
103
+ return input;
104
+ }
105
+ function apiInput(input, mode) {
106
+ const body = {
107
+ kdt_id: input.kdtId, channel: input.channel,
108
+ page_no: mode === 'scan' ? 1 : input.pageNo,
109
+ page_size: mode === 'scan' ? 50 : input.pageSize
110
+ };
111
+ for (const [option, key] of filterFields) {
112
+ if (input[option] !== undefined) body[key] = input[option];
113
+ }
114
+ for (const [option, key] of [['keyword', 'keyword'], ['itemType', 'publish_code'], ['sorts', 'sorts']]) {
115
+ if (input[option] !== undefined) body[key] = input[option];
116
+ }
117
+ if (mode === 'scan') body.sorts = ['CREATED_TIME_DESC', 'ID_DESC'];
118
+ for (const [option, kind] of [['createdAt', 'create'], ['updatedAt', 'update']]) {
119
+ if (!input[option]) continue;
120
+ body.item_range_query ??= {};
121
+ for (const [field, prefix] of [['start', 'min'], ['end', 'max']]) {
122
+ if (input[option][field] !== undefined) body.item_range_query[`${prefix}_${kind}_time`] = parseDateTime(input[option][field]);
123
+ }
124
+ }
125
+ return { scope: { kdtId: input.kdtId, channel: input.channel }, body, limits: {
126
+ maxItems: mode === 'scan' ? input.maxItems : input.pageSize,
127
+ maxRequests: mode === 'scan' ? input.maxRequests : 1,
128
+ maxResponseBytes: MAX_RESPONSE_BYTES, maxOutputBytes: MAX_OUTPUT_BYTES
129
+ } };
130
+ }
131
+ function addFilters(command, mode) {
132
+ const filtered = command.option('--kdt-id <id>', '目标店铺 ID;省略时使用当前上下文店铺')
133
+ .option('--channel <channel>', '渠道:0 网店,1 门店;各渠道商品可能重叠,数量不能直接相加', '0')
134
+ .option('--classification-ids <ids>', '商品分类 ID 列表,最多 20 项')
135
+ .option('--item-ids <ids>', '新版商品 ID 列表,最多 20 项;不是渠道商品 ID')
136
+ .option('--group-ids <ids>', '一级分组 ID 列表,最多 20 项')
137
+ .option('--second-group-ids <ids>', '二级分组 ID 列表,最多 20 项')
138
+ .option('--not-exclude-ability-codes <codes>', '不排除的能力标,10033 使套餐可被搜索,不是只选套餐')
139
+ .option('--is-displays <values>', '上下架筛选列表:0 仓库中,1 出售中/已售罄')
140
+ .option('--sold-status-list <values>', '销售状态筛选列表:0、1、2、3,组合规则见 schema')
141
+ .option('--keyword <text>', '搜索商品标题、商品/SKU 编码及条码')
142
+ .option('--item-type <type>', '商品类型文案或17位发布码;兼容映射及边界见 schema')
143
+ .option('--updated-at <range>', '更新时间闭区间 START..END,可省略一端;北京时间,可带毫秒')
144
+ .addOption(new Option('--format <format>', 'json 或 pretty').choices(['json', 'pretty']).default('json'))
145
+ .option('--dry-run', '仅本地预览请求、条件和预算,不读凭证、不联网');
146
+ return mode === 'scan'
147
+ ? filtered.requiredOption('--created-at <range>', '必填:创建时间闭区间 START..END,必须两端且开始早于结束;北京时间,可带毫秒')
148
+ : filtered.option('--created-at <range>', '创建时间闭区间 START..END,可省略一端;北京时间,可带毫秒');
149
+ }
150
+ module.exports = { API, sortEnums, typeCodes, buildInput, apiInput, addFilters, MAX_RESPONSE_BYTES, MAX_OUTPUT_BYTES };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const { readApi, ItemQueryError, object } = require('./api');
4
+ const { withoutInventory } = require('./normalize');
5
+ const { normalizeMoney } = require('../../../../execution/money');
6
+ const { normalizeTimes } = require('../../../../execution/datetime');
7
+ const { attachH5Items, SHOP_API, ORG_API, PAGE_SIZE, MAX_PAGES } = require('./h5');
8
+ const { API } = require('./search-options');
9
+ const { validateCommandValue } = require('../../../../execution/command-schema');
10
+ const { constraints, modelAt } = require('../../../../execution/error-details');
11
+
12
+ function failure(input, mode, category, stage, sent = 'sent') {
13
+ return new ItemQueryError(category, stage, input.scope, { api: API }, sent, `item.item.${mode}`);
14
+ }
15
+ function preview(input, mode) {
16
+ const search = { method: 'POST', path: API, body: input.body };
17
+ return {
18
+ requests: [search],
19
+ conditionalRequests: [
20
+ { condition: '搜索返回商品时,默认读取授权店铺角色以补 H5;空列表不查角色。',
21
+ request: { method: 'POST', path: SHOP_API, body: {} }, maxRequests: 1 },
22
+ { condition: '授权店铺是总部且目标不同:按页查目标角色,找到即停。',
23
+ request: { method: 'POST', path: ORG_API, body: { page_num: 1, page_size: PAGE_SIZE } }, maxRequests: MAX_PAGES }
24
+ ],
25
+ mode, limits: input.limits,
26
+ strategy: mode === 'list' ? '单页搜索,不自动翻页。' :
27
+ '串行按50/页读取;每片探测总数达到5000时按创建时间闭区间二分,保留其他全部筛选;ID去重,短页才结束,预算耗尽整体失败。'
28
+ };
29
+ }
30
+ async function execute(input, mode, schema) {
31
+ const command = `item.item.${mode}`;
32
+ let requests = 0, responseBytes = 0;
33
+ const resultItems = [], seen = new Set();
34
+ let itemBytes = 2;
35
+ async function page(body) {
36
+ if (requests >= input.limits.maxRequests) throw failure(input, mode, 'budget_exceeded', 'search-requests', 'not_sent');
37
+ requests++;
38
+ const raw = await readApi(API, body, 'search', input.scope, command);
39
+ responseBytes += Buffer.byteLength(JSON.stringify(raw) ?? '');
40
+ if (responseBytes > input.limits.maxResponseBytes) throw failure(input, mode, 'budget_exceeded', 'response-bytes');
41
+ if (!object(raw) || !Array.isArray(raw.items) || raw.items.length > body.page_size ||
42
+ (raw.paginator != null && !object(raw.paginator))) throw failure(input, mode, 'protocol', 'search-page');
43
+ const paginator = raw.paginator || {};
44
+ for (const [key, expected] of [['page_no', body.page_no], ['page_size', body.page_size]]) {
45
+ if (paginator[key] != null && paginator[key] !== expected) throw failure(input, mode, 'protocol', 'search-page');
46
+ }
47
+ if (paginator.total_count != null && (!Number.isSafeInteger(paginator.total_count) || paginator.total_count < 0)) {
48
+ throw failure(input, mode, 'protocol', 'search-page');
49
+ }
50
+ function invalidItem(index, field, stage, reason = 'constraint') {
51
+ const path = `/data/items/${index}${field ? `/${field}` : ''}`;
52
+ const error = failure(input, mode, 'protocol', stage);
53
+ error.cliDetails.pageNo = body.page_no;
54
+ error.issues = [{ source: 'output', path, reason,
55
+ expected: constraints(modelAt(schema, 'output', path)) }];
56
+ return error;
57
+ }
58
+ const items = raw.items.map((item, index) => {
59
+ if (!object(item)) throw invalidItem(index, '', 'search-item', 'invalid_type');
60
+ if (!Number.isSafeInteger(item.item_id) || item.item_id <= 0) {
61
+ throw invalidItem(index, 'item_id', 'search-item', 'invalid_type');
62
+ }
63
+ for (const [field, expected] of [['kdt_id', input.scope.kdtId], ['channel', input.scope.channel]]) {
64
+ if (item[field] != null && item[field] !== expected) throw invalidItem(index, field, 'search-item');
65
+ }
66
+ if (body.item_ids && !body.item_ids.includes(item.item_id)) throw invalidItem(index, 'item_id', 'search-item');
67
+ if (mode === 'scan' && item.created_time != null &&
68
+ (item.created_time < body.item_range_query.min_create_time || item.created_time > body.item_range_query.max_create_time)) {
69
+ throw invalidItem(index, 'created_time', 'search-time-range');
70
+ }
71
+ const value = withoutInventory(item);
72
+ // Normalize declared fields separately so failures identify a static field, never its raw value.
73
+ for (const [field, model] of Object.entries(schema.definitions.item.properties)) {
74
+ if (!Object.hasOwn(value, field) || field === 'h5_url') continue;
75
+ try { value[field] = normalizeTimes(normalizeMoney(value[field], model), model, schema.definitions); }
76
+ catch { throw invalidItem(index, field, 'search-item-model', 'invalid_format'); }
77
+ }
78
+ // H5 is always generated from the verified role, never copied from an extension.
79
+ delete value.h5_url;
80
+ try { validateCommandValue(schema, 'output', output([{ ...value, h5_url: null }], null)); }
81
+ catch (cause) {
82
+ const error = failure(input, mode, 'protocol', 'search-item-model');
83
+ error.cliDetails.pageNo = body.page_no;
84
+ error.issues = (cause.issues || []).map(issue => ({ ...issue,
85
+ path: issue.path.replace(/^\/data\/items\/0(?=\/|$)/, `/data/items/${index}`) }));
86
+ if (cause.issuesTruncated) error.issuesTruncated = true;
87
+ throw error;
88
+ }
89
+ return value;
90
+ });
91
+ return { items, total: paginator.total_count ?? null };
92
+ }
93
+ function output(items, pagination) {
94
+ if (mode === 'scan') return { data: { items, scope: input.scope } };
95
+ return { data: { items, pagination: pagination || {
96
+ pageNo: input.body.page_no, pageSize: input.body.page_size,
97
+ returned: items.length, hasMore: null, nextPageNo: null, windowLimitReached: false
98
+ }, scope: input.scope } };
99
+ }
100
+ function append(items) {
101
+ let added = 0;
102
+ for (const item of items) {
103
+ if (seen.has(item.item_id)) continue;
104
+ seen.add(item.item_id);
105
+ if (resultItems.length >= input.limits.maxItems) throw failure(input, mode, 'budget_exceeded', 'result-items');
106
+ itemBytes += Buffer.byteLength(JSON.stringify(item)) + 1;
107
+ if (itemBytes > input.limits.maxOutputBytes) throw failure(input, mode, 'budget_exceeded', 'output-bytes');
108
+ resultItems.push(item); added++;
109
+ }
110
+ return added;
111
+ }
112
+ let hasMore, total = null;
113
+ if (mode === 'list') {
114
+ const data = await page(input.body);
115
+ // A list is one page. Preserve rows rather than hiding duplicates in its page.
116
+ resultItems.push(...data.items);
117
+ total = data.total;
118
+ hasMore = data.total === null ? null : data.total > input.body.page_no * input.body.page_size;
119
+ } else {
120
+ const initial = input.body.item_range_query;
121
+ const pending = [{ start: initial.min_create_time, end: initial.max_create_time }];
122
+ while (pending.length) {
123
+ const range = pending.pop();
124
+ const base = { ...input.body, item_range_query: { ...initial, min_create_time: range.start, max_create_time: range.end } };
125
+ const first = await page(base);
126
+ if (first.total != null && first.total > input.limits.maxItems) throw failure(input, mode, 'budget_exceeded', 'estimated-items');
127
+ if (first.total != null && first.total >= 5000) {
128
+ if (range.start >= range.end) throw failure(input, mode, 'pagination_limit', 'indivisible-window');
129
+ const mid = range.start + Math.floor((range.end - range.start) / 2);
130
+ // Descending traversal; closed inclusive intervals do not overlap.
131
+ pending.push({ start: range.start, end: mid }, { start: mid + 1, end: range.end });
132
+ continue;
133
+ }
134
+ let current = first;
135
+ for (let pageNo = 1; ; pageNo++) {
136
+ const added = append(current.items);
137
+ if (current.items.length < 50) break;
138
+ if (added === 0) throw failure(input, mode, 'pagination_stalled', 'search-pages');
139
+ if (pageNo === 100) throw failure(input, mode, 'pagination_limit', 'search-pages');
140
+ current = await page({ ...base, page_no: pageNo + 1 });
141
+ if (current.total != null && current.total > 5000) throw failure(input, mode, 'pagination_limit', 'search-changed');
142
+ }
143
+ }
144
+ }
145
+ await attachH5Items(resultItems, input.scope, command);
146
+ const result = output(resultItems, mode === 'list' ? {
147
+ pageNo: input.body.page_no, pageSize: input.body.page_size,
148
+ returned: resultItems.length, hasMore, ...(total === null ? {} : { total }),
149
+ nextPageNo: hasMore === true && (input.body.page_no + 1) * input.body.page_size <= 5000
150
+ ? input.body.page_no + 1 : null,
151
+ windowLimitReached: hasMore !== false && (input.body.page_no + 1) * input.body.page_size > 5000
152
+ } : undefined);
153
+ if (Buffer.byteLength(JSON.stringify(result)) > input.limits.maxOutputBytes) throw failure(input, mode, 'budget_exceeded', 'output-bytes');
154
+ try { validateCommandValue(schema, 'output', result); }
155
+ catch { throw failure(input, mode, 'protocol', 'output-model'); }
156
+ return result;
157
+ }
158
+ module.exports = { execute, preview };