@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.
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 +105 -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 +102 -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,399 @@
1
+ 'use strict';
2
+
3
+ const { executionFor } = require('./request-state');
4
+ const { issueReasons } = require('./error-model');
5
+ const { ParserError } = require('./parser-error');
6
+ const contexts = new WeakMap();
7
+ const reasons = new Set(issueReasons);
8
+ const constraintKeys = ['type', 'enum', 'const', 'minimum', 'maximum', 'exclusiveMinimum',
9
+ 'exclusiveMaximum', 'minLength', 'maxLength', 'minItems', 'maxItems', 'format', 'pattern'];
10
+ const pointer = value => String(value).replaceAll('~', '~0').replaceAll('/', '~1');
11
+ const segments = path => path.split('/').slice(1).map(value => value.replaceAll('~1', '/').replaceAll('~0', '~'));
12
+
13
+ function resolveModel(schema, model) {
14
+ const seen = new Set();
15
+ while (model?.$ref?.startsWith('#/') && !seen.has(model.$ref)) {
16
+ seen.add(model.$ref);
17
+ model = segments(model.$ref.slice(1)).reduce((node, key) => node?.[key], schema);
18
+ }
19
+ return model;
20
+ }
21
+
22
+ function modelAt(schema, section, path) {
23
+ let model = resolveModel(schema, schema[section]);
24
+ for (const key of segments(path)) {
25
+ if (model?.properties && Object.hasOwn(model.properties, key)) model = model.properties[key];
26
+ else if ((model?.type === 'array' || (Array.isArray(model?.type) && model.type.includes('array'))) && /^(0|[1-9]\d*)$/.test(key)) model = model.items;
27
+ else return undefined;
28
+ model = resolveModel(schema, model);
29
+ }
30
+ return model;
31
+ }
32
+
33
+ function constraints(model) {
34
+ return Object.fromEntries(constraintKeys.filter(key => Object.hasOwn(model || {}, key))
35
+ .map(key => [key, structuredClone(model[key])]));
36
+ }
37
+
38
+ function validationIssues(errors, schema, section) {
39
+ const issues = [];
40
+ const seen = new Set();
41
+ const keywords = {
42
+ required: 'required', type: 'invalid_type', enum: 'invalid_choice', const: 'invalid_choice',
43
+ minimum: 'out_of_range', maximum: 'out_of_range', exclusiveMinimum: 'out_of_range',
44
+ exclusiveMaximum: 'out_of_range', minLength: 'out_of_range', maxLength: 'out_of_range',
45
+ minItems: 'out_of_range', maxItems: 'out_of_range', format: 'invalid_format',
46
+ pattern: 'invalid_format', dependencies: 'conditional_required',
47
+ additionalProperties: 'unexpected_property'
48
+ };
49
+ for (const error of errors) {
50
+ if (error.keyword === 'if') continue;
51
+ let path = error.instancePath;
52
+ let relatedPaths;
53
+ if (['required', 'dependencies'].includes(error.keyword)) {
54
+ path += `/${pointer(error.params.missingProperty)}`;
55
+ if (error.keyword === 'dependencies') {
56
+ relatedPaths = [`${error.instancePath}/${pointer(error.params.property)}`];
57
+ }
58
+ }
59
+ if (!modelAt(schema, section, path)) {
60
+ while (path && !modelAt(schema, section, path)) path = path.slice(0, path.lastIndexOf('/'));
61
+ }
62
+ const model = modelAt(schema, section, path);
63
+ if (!model) continue;
64
+ const issue = { source: section, path, reason: keywords[error.keyword] || 'constraint' };
65
+ const expected = constraints(model);
66
+ if (error.keyword === 'additionalProperties') expected.additionalProperties = false;
67
+ if (error.keyword === 'contains') {
68
+ const branch = resolveModel(schema, segments(error.schemaPath.slice(1))
69
+ .slice(0, -1).reduce((node, key) => node?.[key], schema[section]));
70
+ if (branch?.contains) expected.contains = structuredClone(branch.contains);
71
+ }
72
+ if (Object.keys(expected).length) issue.expected = expected;
73
+ if (relatedPaths) issue.relatedPaths = relatedPaths;
74
+ const key = JSON.stringify(issue);
75
+ if (!seen.has(key)) {
76
+ seen.add(key);
77
+ issues.push(issue);
78
+ if (issues.length > 16) return { issues: issues.slice(0, 16), issuesTruncated: true };
79
+ }
80
+ }
81
+ return { issues: issues.slice(0, 16), ...(issues.length > 16 ? { issuesTruncated: true } : {}) };
82
+ }
83
+
84
+ class CommandValueError extends Error {
85
+ constructor(section, errors, schema) {
86
+ super(`Invalid ${section}: ${[...new Set(errors.map(error => error.keyword))].join(', ')}`);
87
+ this.section = section;
88
+ Object.assign(this, validationIssues(errors, schema, section));
89
+ }
90
+ }
91
+
92
+ class InputIssueError extends Error {
93
+ constructor(issues) {
94
+ super('输入参数不符合要求');
95
+ this.section = 'input';
96
+ this.issues = issues;
97
+ }
98
+ }
99
+
100
+ function inputIssue(path, reason, expected, relatedPaths) {
101
+ return new InputIssueError([{ source: 'input', path, reason,
102
+ ...(expected ? { expected } : {}), ...(relatedPaths ? { relatedPaths } : {}) }]);
103
+ }
104
+
105
+ function withInputScope(scope, action) {
106
+ try { return action(); }
107
+ catch (error) {
108
+ if (error instanceof CommandValueError || error instanceof InputIssueError) error.inputScope = scope;
109
+ throw error;
110
+ }
111
+ }
112
+
113
+ function parseIntegerOption(raw, schema, path) {
114
+ const expected = constraints(modelAt(schema, 'input', path));
115
+ if (typeof raw !== 'string' || !/^(0|[1-9]\d*)$/.test(raw)) {
116
+ throw inputIssue(path, 'invalid_format', expected);
117
+ }
118
+ const value = Number(raw);
119
+ if (!Number.isSafeInteger(value) || value < expected.minimum || value > expected.maximum) {
120
+ throw inputIssue(path, 'out_of_range', expected);
121
+ }
122
+ return value;
123
+ }
124
+
125
+ function copyErrorDetails(details, overrides = {}) {
126
+ const result = {};
127
+ for (const key of ['category', 'stage', 'command', 'targetScope', 'requestState', 'nextStep',
128
+ 'issues', 'issuesTruncated', 'errorVersion', 'recovery', 'execution', 'suggestions', 'helpArgv']) {
129
+ if (Object.hasOwn(details, key)) result[key] = structuredClone(details[key]);
130
+ }
131
+ for (const key of ['businessCode', 'gatewayCode']) {
132
+ const value = details[key];
133
+ if (Number.isSafeInteger(value) || (typeof value === 'string' && /^-?\d{1,12}$/.test(value))) result[key] = value;
134
+ }
135
+ for (const key of ['trace_id', 'request_id']) {
136
+ if (typeof details[key] === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(details[key])) result[key] = details[key];
137
+ }
138
+ if (Number.isInteger(details.httpStatus) && details.httpStatus >= 100 && details.httpStatus <= 599) {
139
+ result.httpStatus = details.httpStatus;
140
+ }
141
+ if (Number.isSafeInteger(details.pageNo) && details.pageNo > 0) result.pageNo = details.pageNo;
142
+ if (typeof details.api === 'string' && /^[a-z][a-z0-9._-]*\/\d+\.\d+\.\d+$/.test(details.api)) result.api = details.api;
143
+ return Object.assign(result, overrides);
144
+ }
145
+
146
+ function bindError(error, context) {
147
+ if (error instanceof Error) {
148
+ contexts.set(error, context);
149
+ if (!error.cliDetails && ['input', 'argv'].includes(error.section)) {
150
+ error.cliDetails = {
151
+ category: context.parameterCategory, stage: 'input', command: context.path,
152
+ targetScope: error.inputScope || {}, requestState: 'not_sent',
153
+ nextStep: '按同版契约核对输入;恢复动作以最终错误诊断为准。'
154
+ };
155
+ }
156
+ }
157
+ return error;
158
+ }
159
+
160
+ function optionFor(path, context) {
161
+ let candidate = path;
162
+ while (candidate) {
163
+ const attribute = context.optionPaths?.[candidate] ||
164
+ (segments(candidate).length === 1 ? segments(candidate)[0] : undefined);
165
+ const option = context.command.options.find(value => value.attributeName() === attribute);
166
+ if (option) return option.long || option.short;
167
+ candidate = candidate.slice(0, candidate.lastIndexOf('/'));
168
+ }
169
+ }
170
+
171
+ function safeIssues(error, context) {
172
+ return (error.issues || error.cliDetails?.issues || []).slice(0, 16).flatMap(issue => {
173
+ if (!reasons.has(issue.reason) || !['argv', 'input', 'context', 'output', 'preview'].includes(issue.source)) return [];
174
+ if (issue.path !== undefined && (typeof issue.path !== 'string' ||
175
+ (issue.path !== '' && !issue.path.startsWith('/')) || !modelAt(context.schema,
176
+ ['argv', 'context'].includes(issue.source) ? 'input' : issue.source, issue.path))) return [];
177
+ const result = { source: issue.source, reason: issue.reason };
178
+ if (issue.path !== undefined) result.path = issue.path;
179
+ const option = issue.source === 'context' ? undefined :
180
+ issue.option && context.command.options.some(value =>
181
+ [value.long, value.short].includes(issue.option)) ? issue.option : optionFor(issue.path || '', context);
182
+ if (option) result.option = option;
183
+ if (issue.expected) result.expected = structuredClone(issue.expected);
184
+ if (issue.relatedPaths) result.relatedPaths = issue.relatedPaths.filter(path =>
185
+ modelAt(context.schema, 'input', path));
186
+ return [result];
187
+ });
188
+ }
189
+
190
+ const localContextMessages = Object.freeze({
191
+ context_missing: '执行前需要登录并选择店铺',
192
+ target_missing: '缺少目标店铺;请先登录选店,或在 --dry-run 中显式提供 --kdt-id',
193
+ context_invalid: '当前用户上下文无效,请检查 userId、hq、shopId 和 orgType',
194
+ requires_hq: '当前是店铺上下文,此命令只能在总部上下文执行。',
195
+ requires_shop: '当前是总部上下文,此命令只能在店铺上下文执行。'
196
+ });
197
+
198
+ function localContextReason(error, details, execution) {
199
+ return Object.hasOwn(localContextMessages, error.diagnosticReason || '') &&
200
+ ['configuration', 'permission'].includes(details.category) &&
201
+ ['context', 'configuration'].includes(details.stage) &&
202
+ execution.requestState === 'not_sent' && execution.coverage === 'complete' &&
203
+ execution.observedAttempts === 0 ? error.diagnosticReason : undefined;
204
+ }
205
+
206
+ function describeRecovery(details, execution, missingAuthentication = false, contextReason, responseReason) {
207
+ if (contextReason) return { action: 'stop_and_inspect', nextStep:
208
+ ['context_missing', 'target_missing'].includes(contextReason)
209
+ ? '未登录先执行 zan-saas auth login;登录后用 zan-saas shop list 和 zan-saas shop use <kdtId> 选店。缺少上下文时仅可显式指定店铺做 --dry-run,不自动切店或重放。'
210
+ : contextReason === 'context_invalid'
211
+ ? '检查上下文中的 userId、hq、shopId 和 orgType;不自动切店、换身份或重放。'
212
+ : '请核对本次目标所需的总部或店铺上下文;不自动切店、换身份或重放,权限仍由服务端校验。' };
213
+ if (execution.requestState === 'not_sent' && execution.coverage === 'complete' &&
214
+ execution.observedAttempts === 0 && ['input', 'validation'].includes(details.stage) &&
215
+ ['parameter', 'validation'].includes(details.category) &&
216
+ details.issues?.some(issue => ['argv', 'input'].includes(issue.source))) {
217
+ const fields = [...new Set(details.issues.map(issue => issue.option).filter(Boolean))].join('、');
218
+ return { action: 'correct_input',
219
+ nextStep: `按同版契约修正${fields || '输入参数'};本次未发送请求,修正后可重新构造调用。` };
220
+ }
221
+ if (details.category === 'provider_unavailable' && details.command === 'privacy.field.decrypt') {
222
+ return { action: 'stop_and_inspect', nextStep: '当前仅支持离线预览,请加 --dry-run;真实解密服务尚未接入。' };
223
+ }
224
+ if (missingAuthentication || details.category === 'authentication') {
225
+ return { action: 'complete_authentication',
226
+ nextStep: '检查 zan-saas auth status --format json,按公共认证流程完成登录;不在聊天中提供凭证,不自动重放原调用。' };
227
+ }
228
+ if (gatewayAccessDenied(details)) {
229
+ return { action: 'request_access_review',
230
+ nextStep: '请检查当前账号是否有该店铺权限,以及店铺应用是否已获该接口的开放能力授权;可提供接口和 trace_id 给管理员排查,不自动重放。' };
231
+ }
232
+ if (details.category === 'permission') {
233
+ return { action: 'request_access_review',
234
+ nextStep: '核对原账号、目标店铺与服务端授权,可提供脱敏接口及 trace_id 给管理员;不切店、换身份或自动重放。' };
235
+ }
236
+ if (details.stage === 'shop-binding' && execution.requestState === 'sent') {
237
+ return { action: 'stop_and_inspect',
238
+ nextStep: '店铺核验失败,后续业务请求尚未发送;本次已有核验请求记录,核对原店铺和凭证后停止,不自动切店或重放。' };
239
+ }
240
+ if (responseReason) return { action: 'stop_and_inspect',
241
+ nextStep: `${responseReason.nextStep} 不自动重放、切店或换身份。` };
242
+ if (details.requestState === 'sent' && details.category === 'business_rejected') {
243
+ return { action: 'stop_and_inspect',
244
+ nextStep: '接口已拒绝本次查询;将诊断中的 api、businessCode 或 gatewayCode、trace_id 提供给管理员核查服务端原因。错误码本身不能证明是权限或数据问题,不自动重放、切店或换身份。' };
245
+ }
246
+ if (details.requestState === 'sent' && details.category === 'protocol' &&
247
+ details.issues?.some(issue => issue.source === 'output')) {
248
+ return { action: 'stop_and_inspect',
249
+ nextStep: '接口已返回数据,但 issues 指出的字段不符合本命令契约;提供字段路径、预期类型及 api、trace_id 核查响应和适配,不将失败解释为空结果,不自动重放。' };
250
+ }
251
+ return { action: 'stop_and_inspect',
252
+ nextStep: execution.requestState === 'unknown' ?
253
+ '本次请求或业务结果可能未知,停止并核对脱敏诊断;不能视为未执行,不自动重放。' :
254
+ execution.observedAttempts ?
255
+ '本次已有请求发送,当前阶段状态见 requestState;停止并核对脱敏诊断,不把失败解释为空结果,不自动重放。' :
256
+ '核对本次上下文、配置及同版契约后停止处理;不自动切店、换身份或重放。' };
257
+ }
258
+
259
+ // Response adapters may select a reason only from the leaf's static, same-version contract.
260
+ // Never use upstream free text as a diagnostic message or guess the original failure field.
261
+ function declaredResponseReason(error, context, details, execution) {
262
+ const entries = context.schema.errors?.responseReasons;
263
+ const key = error.diagnosticReason;
264
+ if (typeof key !== 'string' || !entries || !Object.hasOwn(entries, key) ||
265
+ details.requestState !== 'sent' || execution.requestState !== 'sent' ||
266
+ execution.coverage !== 'complete' || execution.observedAttempts === 0) return undefined;
267
+ const reason = entries[key];
268
+ return reason && reason.category === details.category && reason.stage === details.stage &&
269
+ typeof reason.message === 'string' && typeof reason.nextStep === 'string' ? reason : undefined;
270
+ }
271
+
272
+ function businessRejectionMessage(details) {
273
+ const code = details.gatewayCode !== undefined ? `网关码 ${details.gatewayCode}` :
274
+ details.businessCode !== undefined ? `业务码 ${details.businessCode}` : undefined;
275
+ return `接口拒绝了本次查询${code ? `(${code})` : ''},已停止处理。`;
276
+ }
277
+
278
+ function gatewayAccessDenied(details) {
279
+ return details?.requestState === 'sent' &&
280
+ ['business_rejected', 'permission'].includes(details.category) &&
281
+ [4204, '4204'].includes(details.gatewayCode);
282
+ }
283
+
284
+ function finalizeError(error, registeredContext) {
285
+ if (error instanceof ParserError) return structuredClone(error.result);
286
+ const context = contexts.get(error) || registeredContext;
287
+ if (context?.schema.errors?.diagnosticsVersion !== 2) {
288
+ if (gatewayAccessDenied(error.cliDetails)) {
289
+ error.message = '网关拒绝访问:没有操作权限。';
290
+ error.cliDetails.nextStep = '请检查当前账号是否有该店铺权限,以及店铺应用是否已获该接口的开放能力授权;可提供接口和 trace_id 给管理员排查。';
291
+ }
292
+ return { success: false, code: -10001, message: error.message,
293
+ ...(error.cliDetails ? { details: error.cliDetails } : {}) };
294
+ }
295
+ let execution = executionFor(error);
296
+ if (execution.requestState === 'not_sent' &&
297
+ ['sent', 'unknown'].includes(error.cliDetails?.requestState)) {
298
+ execution = { ...execution, requestState: 'unknown', coverage: 'partial' };
299
+ }
300
+ try {
301
+ return diagnosticError(error, context, execution);
302
+ } catch {
303
+ return { success: false, code: -10001, message: '命令执行失败,已停止处理',
304
+ details: {
305
+ category: 'protocol', stage: 'output-model', command: context.path, targetScope: {},
306
+ requestState: 'unknown', errorVersion: 2, execution,
307
+ recovery: { action: 'stop_and_inspect', automaticBusinessRetry: false },
308
+ nextStep: '诊断信息不符合本命令契约;停止核查,不推定未执行,不自动重放。'
309
+ } };
310
+ }
311
+ }
312
+
313
+ function issueMessage(issues) {
314
+ const first = issues.find(issue => issue.option);
315
+ if (!first) {
316
+ const parserMessages = { unknown_option: '命令中有不支持的选项,请查看此命令 --help',
317
+ unknown_command: '命令路径不支持,请查看 --help',
318
+ excess_arguments: '命令中有多余的位置参数,请查看此命令 --help',
319
+ required: '缺少必填参数,请查看此命令 --help' };
320
+ const parserIssue = issues.find(issue => issue.source === 'argv' && parserMessages[issue.reason]);
321
+ if (parserIssue) return parserMessages[parserIssue.reason];
322
+ return issues.find(issue => ['constraint', 'conditional_required'].includes(issue.reason) && issue.expected?.format)?.expected.format || '输入参数不符合要求';
323
+ }
324
+ const { option, reason, expected = {} } = first;
325
+ if (reason === 'required') return `请提供 ${option}`;
326
+ if (reason === 'missing_value') return `请为 ${option} 提供值`;
327
+ if (reason === 'out_of_range' && expected.minimum !== undefined && expected.maximum !== undefined) {
328
+ return `${option} 必须在 ${expected.minimum} 到 ${expected.maximum} 之间`;
329
+ }
330
+ if (expected.enum) return `${option} 只能使用:${expected.enum.join('、')}`;
331
+ if (expected.items?.enum) return `${option} 只能使用:${expected.items.enum.join('、')};多个值用逗号分隔`;
332
+ if (reason === 'invalid_format' && expected.format) {
333
+ return `${option} 格式不正确:${expected.format}`;
334
+ }
335
+ if (['constraint', 'conditional_required'].includes(reason) && expected.format) return `${option}:${expected.format}`;
336
+ if (first.relatedPaths?.length) return `${option} 与其他参数组合不符合要求,请核对关联条件`;
337
+ return `${option} 不符合要求,请按 issues.expected 修正`;
338
+ }
339
+
340
+ function diagnosticError(error, context, execution) {
341
+ const inputError = ['input', 'argv'].includes(error.section);
342
+ const details = copyErrorDetails(error.cliDetails || {}, {
343
+ category: error.cliDetails?.category || (inputError ? context.parameterCategory : 'protocol'),
344
+ stage: error.cliDetails?.stage || (inputError ? 'input' : 'output-model'),
345
+ command: context.path, targetScope: {}, requestState: error.cliDetails?.requestState ||
346
+ (inputError ? 'not_sent' : 'unknown'), errorVersion: 2, execution
347
+ });
348
+ const sourceScope = error.cliDetails?.targetScope || error.inputScope || {};
349
+ const scopeModel = context.schema.error.properties.details.properties.targetScope;
350
+ // Preserve only static declared scalar scope fields; union schemas may also allow an empty scope.
351
+ const branches = scopeModel.oneOf || scopeModel.anyOf || [scopeModel];
352
+ for (const branch of branches) {
353
+ const model = resolveModel(context.schema, branch);
354
+ if (!model?.properties) continue;
355
+ const scope = {};
356
+ for (const [name, field] of Object.entries(model.properties)) {
357
+ const target = resolveModel(context.schema, field);
358
+ const value = sourceScope[name];
359
+ if (target?.type === 'string' && name === 'kdtId' && typeof value === 'string' &&
360
+ /^[1-9]\d*$/.test(value) && Number.isSafeInteger(Number(value))) scope[name] = value;
361
+ if (target?.type === 'integer' && Number.isSafeInteger(value) &&
362
+ (name !== 'kdtId' || value > 0) && (!target.enum || target.enum.includes(value))) scope[name] = value;
363
+ }
364
+ if ((model.required || []).every(name => Object.hasOwn(scope, name))) {
365
+ details.targetScope = scope;
366
+ break;
367
+ }
368
+ }
369
+ const issues = safeIssues(error, context);
370
+ delete details.issues;
371
+ if (issues.length) details.issues = issues;
372
+ if (error.issuesTruncated) details.issuesTruncated = true;
373
+ const contextReason = localContextReason(error, details, execution);
374
+ const responseReason = declaredResponseReason(error, context, details, execution);
375
+ const recovery = describeRecovery(details, execution, error.diagnosticReason === 'missing_authentication', contextReason, responseReason);
376
+ const groupConflict = inputError && context.path === 'item.group.list' &&
377
+ issues.some(issue => issue.path === '/parentGroupId' && issue.reason === 'constraint' && issue.relatedPaths?.includes('/title'))
378
+ ? context.schema.errors.parentTitleConflict : undefined;
379
+ if (groupConflict && recovery.action === 'correct_input') recovery.nextStep = groupConflict.nextStep;
380
+ details.recovery = { action: recovery.action, automaticBusinessRetry: false };
381
+ details.nextStep = recovery.nextStep;
382
+ const result = { success: false, code: -10001,
383
+ message: inputError ? (groupConflict?.message || issueMessage(issues)) :
384
+ contextReason ? localContextMessages[contextReason] :
385
+ gatewayAccessDenied(details) ? '网关拒绝访问:没有操作权限。' :
386
+ details.category === 'provider_unavailable' && context.path === 'privacy.field.decrypt' ? '当前仅支持离线预览,真实解密服务尚未接入' :
387
+ responseReason ? responseReason.message :
388
+ details.category === 'business_rejected' && details.requestState === 'sent' ? businessRejectionMessage(details) :
389
+ issues.some(issue => issue.source === 'output') ?
390
+ `返回数据字段 ${issues.find(issue => issue.source === 'output').path} 不符合要求,已停止查询` :
391
+ '命令执行失败,详情见安全诊断', details };
392
+ require('./command-schema').validateCommandValue(context.schema, 'error', result);
393
+ return result;
394
+ }
395
+
396
+ module.exports = {
397
+ CommandValueError, InputIssueError, inputIssue, parseIntegerOption, copyErrorDetails,
398
+ bindError, finalizeError, describeRecovery, modelAt, constraints, withInputScope
399
+ };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const { isDeepStrictEqual } = require('node:util');
4
+ const object = (properties, required = Object.keys(properties)) => ({
5
+ type: 'object', additionalProperties: false, required, properties
6
+ });
7
+ const text = description => ({ type: 'string', description });
8
+ const integer = description => ({ type: 'integer', description });
9
+ const number = description => ({ type: 'number', description });
10
+ const choices = (values, description) => ({ enum: values, description });
11
+ const scalar = { anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }, { type: 'null' }],
12
+ description: '契约声明的静态标量约束。' };
13
+ const issueReasons = ['required', 'invalid_type', 'invalid_choice', 'out_of_range',
14
+ 'invalid_format', 'conditional_required', 'constraint', 'unexpected_property',
15
+ 'unknown_option', 'unknown_command', 'excess_arguments', 'missing_value'];
16
+ const definitions = {
17
+ diagnosticExpected: object({
18
+ type: { anyOf: [text('预期的 JSON 类型。'), {
19
+ type: 'array', items: text('预期的 JSON 类型。')
20
+ }], description: '同版模型允许的类型。' },
21
+ enum: { type: 'array', items: scalar, description: '同版模型允许的静态值。' },
22
+ const: scalar,
23
+ minimum: number('包含的最小值。'), maximum: number('包含的最大值。'),
24
+ exclusiveMinimum: number('不包含的最小值。'), exclusiveMaximum: number('不包含的最大值。'),
25
+ minLength: integer('最少字符数。'), maxLength: integer('最多字符数。'),
26
+ minItems: integer('最少元素数。'), maxItems: integer('最多元素数。'),
27
+ format: text('同版模型声明的格式。'), pattern: text('同版模型声明的字符约束。'),
28
+ additionalProperties: { const: false, description: '对象拒绝契约以外的属性。' },
29
+ contains: object({ const: scalar }),
30
+ items: object({ enum: { type: 'array', items: scalar, description: '元素允许的静态值。' } })
31
+ }, []),
32
+ diagnosticIssue: object({
33
+ source: choices(['argv', 'input', 'context', 'output', 'preview'], '问题所属的参数、输入、上下文、结果或预览模型。'),
34
+ path: text('对应模型中的 JSON Pointer;不含原始输入或动态字典键。'),
35
+ option: text('可定位时给出的已注册 CLI 选项。'),
36
+ reason: choices(issueReasons, '字段违反的约束类型,原始值不回显。'),
37
+ expected: { $ref: '#/definitions/diagnosticExpected', description: '同版契约的静态预期。' },
38
+ relatedPaths: { type: 'array', items: text('关联字段的 JSON Pointer。'), description: '触发条件约束的关联字段。' }
39
+ }, ['source', 'reason']),
40
+ diagnosticRecovery: object({
41
+ action: choices(['correct_input', 'complete_authentication', 'request_access_review', 'stop_and_inspect'],
42
+ '依次表示修正本地输入、补认证、核对权限或停止核查;仅是说明,不执行操作或授予权限。'),
43
+ automaticBusinessRetry: { const: false, description: '不据此自动重放业务请求。' }
44
+ }),
45
+ diagnosticExecution: object({
46
+ requestState: choices(['not_sent', 'sent', 'unknown'], '整次调用尚未发送、已有发送记录或无法排除未知请求;不代表业务完成。'),
47
+ coverage: choices(['complete', 'partial'], '请求观察覆盖全部已接入路径,或存在未观测路径;部分覆盖不能声明零请求。'),
48
+ observedAttempts: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER,
49
+ description: '本地已进入发送边界的观测次数下界,不是服务端执行次数。' }
50
+ })
51
+ };
52
+ const properties = {
53
+ errorVersion: { const: 2, description: '本命令采用错误诊断协议第二版。' },
54
+ issues: { type: 'array', minItems: 1, maxItems: 16, items: { $ref: '#/definitions/diagnosticIssue' },
55
+ description: '最多十六个已定位问题,不包含收到的原值。' },
56
+ issuesTruncated: { const: true, description: '问题超过返回上限,当前列表没有穷尽全部问题。' },
57
+ recovery: { $ref: '#/definitions/diagnosticRecovery', description: '受限的恢复说明,不会自动执行。' },
58
+ execution: { $ref: '#/definitions/diagnosticExecution', description: '整次调用的请求观察,独立于失败阶段的 requestState。' }
59
+ };
60
+
61
+ function applyDiagnosticModel(schema) {
62
+ schema.definitions = { ...schema.definitions, ...structuredClone(definitions) };
63
+ Object.assign(schema.error.properties.details.properties, structuredClone(properties));
64
+ schema.error.properties.details.required = [...new Set([
65
+ ...schema.error.properties.details.required, 'errorVersion', 'recovery', 'execution'
66
+ ])];
67
+ schema.errors.diagnosticsVersion = 2;
68
+ return schema;
69
+ }
70
+
71
+ function assertDiagnosticModel(schema) {
72
+ if (schema.errors?.diagnosticsVersion !== 2) return;
73
+ const details = schema.error?.properties?.details;
74
+ if (!Object.entries(definitions).every(([key, value]) => isDeepStrictEqual(schema.definitions?.[key], value)) ||
75
+ !Object.entries(properties).every(([key, value]) => isDeepStrictEqual(details?.properties?.[key], value)) ||
76
+ !['errorVersion', 'recovery', 'execution'].every(key => details?.required?.includes(key))) {
77
+ throw new Error('Diagnostic contract differs from shared model; run node scripts/sync-error-diagnostics.js');
78
+ }
79
+ }
80
+
81
+ module.exports = { applyDiagnosticModel, assertDiagnosticModel, issueReasons,
82
+ diagnosticDefinitions: definitions };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ function exactNumber(source) {
4
+ const number = Number(source);
5
+ if (!Number.isInteger(number)) return number;
6
+ const match = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/.exec(source);
7
+ const fraction = match[3] || '';
8
+ const significant = `${match[2]}${fraction}`.replace(/^0+/, '');
9
+ const digits = significant.replace(/0+$/, '');
10
+ if (!digits) return number;
11
+ if (number === 0) return NaN;
12
+ const scale = Number(match[4] || 0) - fraction.length + significant.length - digits.length;
13
+ const integer = BigInt(number).toString();
14
+ const magnitude = integer.startsWith('-') ? integer.slice(1) : integer;
15
+ if (scale < 0 || digits.length + scale !== magnitude.length) return NaN;
16
+ return `${match[1]}${digits}${'0'.repeat(scale)}` === integer ? number : NaN;
17
+ }
18
+
19
+ function parseExactJson(text, decodeNumber = exactNumber, { rejectDuplicates = false } = {}) {
20
+ JSON.parse(text);
21
+ let index = 0;
22
+ let result;
23
+ const stack = [];
24
+ const numberPattern = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
25
+ function append(value) {
26
+ if (stack.length === 0) result = value;
27
+ else {
28
+ const parent = stack[stack.length - 1];
29
+ if (Array.isArray(parent.value)) parent.value.push(value);
30
+ else {
31
+ if (rejectDuplicates && Object.hasOwn(parent.value, parent.key)) {
32
+ throw new Error('duplicate JSON member');
33
+ }
34
+ Object.defineProperty(parent.value, parent.key, {
35
+ value, enumerable: true, configurable: true, writable: true
36
+ });
37
+ parent.key = null;
38
+ }
39
+ }
40
+ }
41
+ while (index < text.length) {
42
+ const token = text[index++];
43
+ if (token.charCodeAt(0) <= 32 || token === ':' || token === ',') continue;
44
+ if (token === '{' || token === '[') {
45
+ const value = token === '{' ? {} : [];
46
+ append(value);
47
+ stack.push({ value, key: null });
48
+ } else if (token === '}' || token === ']') {
49
+ stack.pop();
50
+ } else if (token === '"') {
51
+ const start = index - 1;
52
+ while (index < text.length) {
53
+ const character = text[index++];
54
+ if (character === '\\') index++;
55
+ else if (character === '"') break;
56
+ }
57
+ const value = JSON.parse(text.slice(start, index));
58
+ const parent = stack[stack.length - 1];
59
+ if (parent && !Array.isArray(parent.value) && parent.key === null) parent.key = value;
60
+ else append(value);
61
+ } else if (token === 't') {
62
+ index += 3;
63
+ append(true);
64
+ } else if (token === 'f') {
65
+ index += 4;
66
+ append(false);
67
+ } else if (token === 'n') {
68
+ index += 3;
69
+ append(null);
70
+ } else {
71
+ numberPattern.lastIndex = index - 1;
72
+ const number = numberPattern.exec(text)[0];
73
+ index = numberPattern.lastIndex;
74
+ append(decodeNumber(number));
75
+ }
76
+ }
77
+ return result;
78
+ }
79
+
80
+ module.exports = { parseExactJson };
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ function readTotal(source, field, invalid = () => new TypeError('Invalid list total')) {
4
+ const total = source[field];
5
+ if (total === undefined || total === null) return null;
6
+ if (!Number.isSafeInteger(total) || total < 0) throw invalid();
7
+ return total;
8
+ }
9
+
10
+ module.exports = { readTotal };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ function fenToYuan(value) {
4
+ if (!Number.isSafeInteger(value)) throw new Error('Amount must be a safe integer in fen');
5
+ const amount = BigInt(value);
6
+ const absolute = amount < 0n ? -amount : amount;
7
+ return `${amount < 0n ? '-' : ''}${absolute / 100n}.${String(absolute % 100n).padStart(2, '0')}`;
8
+ }
9
+
10
+ function formatYuan(value) {
11
+ // The API explicitly returns an empty string for some origin_price values.
12
+ if (value === '') return value;
13
+ if (typeof value !== 'string' || value.trim() !== value || !/^-?\d+(?:\.\d+)?$/.test(value)) {
14
+ throw new Error('Amount must be decimal text in yuan');
15
+ }
16
+ const negative = value.startsWith('-');
17
+ const [whole, fraction = ''] = (negative ? value.slice(1) : value).split('.');
18
+ if (/[1-9]/.test(fraction.slice(2))) throw new Error('Amount cannot be represented exactly in fen');
19
+ const cents = BigInt(whole) * 100n + BigInt(fraction.slice(0, 2).padEnd(2, '0'));
20
+ return `${negative && cents !== 0n ? '-' : ''}${cents / 100n}.${String(cents % 100n).padStart(2, '0')}`;
21
+ }
22
+
23
+ function yuanToFenBigInt(value) {
24
+ const normalized = formatYuan(value);
25
+ if (normalized === '') throw new Error('Amount must be decimal text in yuan');
26
+ return BigInt(normalized.replace('.', ''));
27
+ }
28
+
29
+ function yuanToFen(value) {
30
+ const cents = yuanToFenBigInt(value);
31
+ const result = Number(cents);
32
+ if (!Number.isSafeInteger(result)) throw new Error('Amount exceeds safe integer fen');
33
+ return result;
34
+ }
35
+
36
+ function compareYuan(left, right) {
37
+ const a = yuanToFenBigInt(left);
38
+ const b = yuanToFenBigInt(right);
39
+ return a < b ? -1 : a > b ? 1 : 0;
40
+ }
41
+
42
+ function normalizeMoney(value, model) {
43
+ if (value == null) return value;
44
+ if (model.$comment === 'upstream-money:fen') return fenToYuan(value);
45
+ if (model.$comment === 'upstream-money:yuan') return formatYuan(value);
46
+ if (Array.isArray(value) && model.items) {
47
+ return value.map(entry => normalizeMoney(entry, model.items));
48
+ }
49
+ if (typeof value === 'object' && !Array.isArray(value) && model.properties) {
50
+ const result = { ...value };
51
+ for (const [key, child] of Object.entries(model.properties)) {
52
+ if (Object.hasOwn(value, key)) result[key] = normalizeMoney(value[key], child);
53
+ }
54
+ return result;
55
+ }
56
+ return value;
57
+ }
58
+
59
+ module.exports = { normalizeMoney, formatYuan, yuanToFen, compareYuan };