@alipay/alipay-aipay 1.7.0 → 1.7.2

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 (169) hide show
  1. package/README.md +30 -0
  2. package/dist/commands/install-current.js +15 -7
  3. package/dist/commands/install.js +20 -16
  4. package/dist/commands/uninstall.js +8 -3
  5. package/dist/commands/update.js +34 -16
  6. package/dist/index.js +10 -4
  7. package/dist/utils/channels.js +67 -0
  8. package/dist/utils/config.js +15 -11
  9. package/dist/utils/file.js +23 -10
  10. package/dist/utils/npm.js +19 -33
  11. package/package.json +8 -3
  12. package/skill/SKILL.md +2 -0
  13. package/skill/references/normal/json/release-channel.json +1 -0
  14. package/skill/references/normal/scripts/self_update.mjs +62 -17
  15. package/skill/references/normal/self-update.md +3 -1
  16. package/skill-pre/SKILL.md +65 -0
  17. package/skill-pre/VERSION +1 -0
  18. package/skill-pre/references/integration/flow.md +602 -0
  19. package/skill-pre/references/integration/modules/alipay-sdk-reminder.md +281 -0
  20. package/skill-pre/references/integration/modules/checklist.md +154 -0
  21. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +199 -0
  22. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +68 -0
  23. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +83 -0
  24. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +80 -0
  25. package/skill-pre/references/integration/modules/code-examples/csharp/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +73 -0
  26. package/skill-pre/references/integration/modules/code-examples/csharp/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +62 -0
  27. package/skill-pre/references/integration/modules/code-examples/csharp/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +132 -0
  28. package/skill-pre/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +640 -0
  29. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +184 -0
  30. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +72 -0
  31. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +96 -0
  32. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +81 -0
  33. package/skill-pre/references/integration/modules/code-examples/java/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +77 -0
  34. package/skill-pre/references/integration/modules/code-examples/java/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +69 -0
  35. package/skill-pre/references/integration/modules/code-examples/java/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +138 -0
  36. package/skill-pre/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.md +623 -0
  37. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +135 -0
  38. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +51 -0
  39. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +63 -0
  40. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
  41. package/skill-pre/references/integration/modules/code-examples/nodejs/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +54 -0
  42. package/skill-pre/references/integration/modules/code-examples/nodejs/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +84 -0
  43. package/skill-pre/references/integration/modules/code-examples/nodejs/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +71 -0
  44. package/skill-pre/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +498 -0
  45. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +157 -0
  46. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
  47. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +73 -0
  48. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +69 -0
  49. package/skill-pre/references/integration/modules/code-examples/php/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +65 -0
  50. package/skill-pre/references/integration/modules/code-examples/php/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +59 -0
  51. package/skill-pre/references/integration/modules/code-examples/php/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +110 -0
  52. package/skill-pre/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +806 -0
  53. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//345/274/202/346/255/245/351/200/232/347/237/245/345/244/204/347/220/206/344/273/243/347/240/201/347/244/272/344/276/213.md +128 -0
  54. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/345/205/263/351/227/255/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +57 -0
  55. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +70 -0
  56. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +65 -0
  57. package/skill-pre/references/integration/modules/code-examples/python/1-/351/200/232/347/224/250/346/216/245/345/217/243//347/273/237/344/270/200/346/224/266/345/215/225/344/272/244/346/230/223/351/200/200/346/254/276/346/237/245/350/257/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +62 -0
  58. package/skill-pre/references/integration/modules/code-examples/python/2-/347/275/221/347/253/231/346/224/257/344/273/230//347/273/237/344/270/200/346/224/266/345/215/225/344/270/213/345/215/225/345/271/266/346/224/257/344/273/230/351/241/265/351/235/242/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +51 -0
  59. package/skill-pre/references/integration/modules/code-examples/python/3-APP/346/224/257/344/273/230/APP/346/224/257/344/273/230/346/216/245/345/217/243/344/273/243/347/240/201/347/244/272/344/276/213.md +47 -0
  60. package/skill-pre/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.md +541 -0
  61. package/skill-pre/references/integration/modules/interface-contract/aipay-interface-contract.md +52 -0
  62. package/skill-pre/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
  63. package/skill-pre/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
  64. package/skill-pre/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
  65. package/skill-pre/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
  66. package/skill-pre/references/integration/modules/interface-guide.md +61 -0
  67. package/skill-pre/references/integration/modules/product-decision.md +73 -0
  68. package/skill-pre/references/integration/modules/sandbox/a2m-sandbox-test.md +235 -0
  69. package/skill-pre/references/integration/modules/sandbox/alipay-sandbox-tool.md +142 -0
  70. package/skill-pre/references/integration/modules/sandbox/sandbox-setup-guide.md +239 -0
  71. package/skill-pre/references/integration/modules/scripts/local_402_sandbox_pay.py +900 -0
  72. package/skill-pre/references/integration/modules/scripts/sandbox_config.sh +347 -0
  73. package/skill-pre/references/integration/modules/troubleshooting-flow.md +28 -0
  74. package/skill-pre/references/normal/alipay-cli-env.md +169 -0
  75. package/skill-pre/references/normal/conversation-telemetry.md +101 -0
  76. package/skill-pre/references/normal/full-process-routing.md +92 -0
  77. package/skill-pre/references/normal/integration-worker.md +90 -0
  78. package/skill-pre/references/normal/json/customer-messages.json +1 -0
  79. package/skill-pre/references/normal/json/managed-jq-assets.json +1 -0
  80. package/skill-pre/references/normal/json/policy-rules.json +1 -0
  81. package/skill-pre/references/normal/json/release-channel.json +1 -0
  82. package/skill-pre/references/normal/rejection-guide.md +41 -0
  83. package/skill-pre/references/normal/scripts/agent_platform.mjs +43 -0
  84. package/skill-pre/references/normal/scripts/alipay_cli_env.mjs +34 -0
  85. package/skill-pre/references/normal/scripts/alipay_cli_env.sh +55 -0
  86. package/skill-pre/references/normal/scripts/alipay_cli_refresh.mjs +129 -0
  87. package/skill-pre/references/normal/scripts/auth_runtime_runner.mjs +60 -0
  88. package/skill-pre/references/normal/scripts/checklist_sections.mjs +43 -0
  89. package/skill-pre/references/normal/scripts/common.sh +365 -0
  90. package/skill-pre/references/normal/scripts/conversation_telemetry.mjs +105 -0
  91. package/skill-pre/references/normal/scripts/detect_dev_tool.sh +79 -0
  92. package/skill-pre/references/normal/scripts/flow_loader.mjs +237 -0
  93. package/skill-pre/references/normal/scripts/integration_context_runner.mjs +151 -0
  94. package/skill-pre/references/normal/scripts/integration_message_runner.mjs +1456 -0
  95. package/skill-pre/references/normal/scripts/interaction_reply.mjs +140 -0
  96. package/skill-pre/references/normal/scripts/managed_jq.mjs +215 -0
  97. package/skill-pre/references/normal/scripts/onboarding_discovery_runner.mjs +387 -0
  98. package/skill-pre/references/normal/scripts/onboarding_message_runner.mjs +1425 -0
  99. package/skill-pre/references/normal/scripts/onboarding_recovery_runner.mjs +100 -0
  100. package/skill-pre/references/normal/scripts/open_official_url.sh +205 -0
  101. package/skill-pre/references/normal/scripts/platform_compat.mjs +42 -0
  102. package/skill-pre/references/normal/scripts/preview_runtime.mjs +151 -0
  103. package/skill-pre/references/normal/scripts/project_route_inspector.sh +132 -0
  104. package/skill-pre/references/normal/scripts/python_script_runner.mjs +58 -0
  105. package/skill-pre/references/normal/scripts/render_customer_message.mjs +1792 -0
  106. package/skill-pre/references/normal/scripts/runtime.mjs +271 -0
  107. package/skill-pre/references/normal/scripts/self_update.mjs +376 -0
  108. package/skill-pre/references/normal/scripts/telemetry.mjs +2102 -0
  109. package/skill-pre/references/normal/self-update.md +7 -0
  110. package/skill-pre/references/normal/telemetry.md +70 -0
  111. package/skill-pre/references/onboarding/assets/keygen.html +927 -0
  112. package/skill-pre/references/onboarding/flow.md +607 -0
  113. package/skill-pre/references/onboarding/modules/app-release.md +90 -0
  114. package/skill-pre/references/onboarding/modules/authorization.md +43 -0
  115. package/skill-pre/references/onboarding/modules/error-handling.md +62 -0
  116. package/skill-pre/references/onboarding/modules/product-sign.md +49 -0
  117. package/skill-pre/references/onboarding/modules/scripts/app.sh +1485 -0
  118. package/skill-pre/references/onboarding/modules/scripts/ar_sign_apply.sh +331 -0
  119. package/skill-pre/references/onboarding/modules/scripts/auth.sh +1040 -0
  120. package/skill-pre/references/onboarding/modules/scripts/download_key_tool.sh +4 -0
  121. package/skill-pre/references/onboarding/modules/scripts/error_handler.sh +841 -0
  122. package/skill-pre/references/onboarding/modules/scripts/mcc.sh +1085 -0
  123. package/skill-pre/references/onboarding/modules/scripts/network_retry.sh +135 -0
  124. package/skill-pre/references/onboarding/modules/scripts/query_sign_status.sh +105 -0
  125. package/skill-pre/references/onboarding/modules/scripts/service.sh +528 -0
  126. package/skill-pre/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
  127. package/skill-pre/references/onboarding/modules/scripts/upload_screenshots.sh +208 -0
  128. package/skill-pre/references/onboarding/modules/scripts/upload_special_license.sh +195 -0
  129. package/skill-pre/references/onboarding/modules/service-registration.md +40 -0
  130. package/skill-pre/references/runtime-entry.md +99 -0
  131. package/dist/commands/install-current.d.ts +0 -4
  132. package/dist/commands/install-current.d.ts.map +0 -1
  133. package/dist/commands/install-current.js.map +0 -1
  134. package/dist/commands/install.d.ts +0 -10
  135. package/dist/commands/install.d.ts.map +0 -1
  136. package/dist/commands/install.js.map +0 -1
  137. package/dist/commands/prepare-tools.d.ts +0 -5
  138. package/dist/commands/prepare-tools.d.ts.map +0 -1
  139. package/dist/commands/prepare-tools.js.map +0 -1
  140. package/dist/commands/uninstall.d.ts +0 -10
  141. package/dist/commands/uninstall.d.ts.map +0 -1
  142. package/dist/commands/uninstall.js.map +0 -1
  143. package/dist/commands/update.d.ts +0 -10
  144. package/dist/commands/update.d.ts.map +0 -1
  145. package/dist/commands/update.js.map +0 -1
  146. package/dist/index.d.ts +0 -2
  147. package/dist/index.d.ts.map +0 -1
  148. package/dist/index.js.map +0 -1
  149. package/dist/types.d.ts +0 -42
  150. package/dist/types.d.ts.map +0 -1
  151. package/dist/types.js.map +0 -1
  152. package/dist/utils/alipay-cli.d.ts +0 -20
  153. package/dist/utils/alipay-cli.d.ts.map +0 -1
  154. package/dist/utils/alipay-cli.js.map +0 -1
  155. package/dist/utils/config.d.ts +0 -52
  156. package/dist/utils/config.d.ts.map +0 -1
  157. package/dist/utils/config.js.map +0 -1
  158. package/dist/utils/file.d.ts +0 -77
  159. package/dist/utils/file.d.ts.map +0 -1
  160. package/dist/utils/file.js.map +0 -1
  161. package/dist/utils/jq.d.ts +0 -17
  162. package/dist/utils/jq.d.ts.map +0 -1
  163. package/dist/utils/jq.js.map +0 -1
  164. package/dist/utils/npm.d.ts +0 -17
  165. package/dist/utils/npm.d.ts.map +0 -1
  166. package/dist/utils/npm.js.map +0 -1
  167. package/dist/utils/shell.d.ts +0 -24
  168. package/dist/utils/shell.d.ts.map +0 -1
  169. package/dist/utils/shell.js.map +0 -1
@@ -0,0 +1,900 @@
1
+ #!/usr/bin/env python3
2
+ """执行本地服务的支付宝沙箱 Payment-Needed 支付流程。"""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import base64
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import re
12
+ import shutil
13
+ import stat
14
+ import subprocess
15
+ import sys
16
+ import tempfile
17
+ import time
18
+ from pathlib import Path
19
+ from typing import Any
20
+ from urllib.parse import quote
21
+
22
+
23
+ PAY_ENDPOINT = "http://aicashier.dl.alipaydev.com/openclawpay/agent/v1/pay"
24
+ PAY_URL_PREFIX = "https://render.alipay.com/p/yuyan/180020010001290755/pay.html?schema="
25
+ A2M_SANDBOX_SERVICE_ID = "api_mock_service_id"
26
+ PAYMENT_NEEDED_REQUIRED_PATHS = (
27
+ ("protocol", "out_trade_no"),
28
+ ("protocol", "amount"),
29
+ ("protocol", "currency"),
30
+ ("protocol", "resource_id"),
31
+ ("protocol", "pay_before"),
32
+ ("protocol", "seller_signature"),
33
+ ("protocol", "seller_sign_type"),
34
+ ("protocol", "seller_unique_id"),
35
+ ("method", "seller_name"),
36
+ ("method", "seller_id"),
37
+ ("method", "seller_app_id"),
38
+ ("method", "goods_name"),
39
+ ("method", "seller_unique_id_key"),
40
+ ("method", "service_id"),
41
+ )
42
+ DEFAULT_PAY_RETRIES = 3
43
+ DEFAULT_PAY_RETRY_DELAY_SECONDS = 2.0
44
+ RETRYABLE_PAY_ERROR_CODES = {"PAY_SUBMIT_FAILED"}
45
+ ARTIFACT_PREFIX = "alipay_local_402_sandbox_pay_"
46
+ ARTIFACT_FILES = {
47
+ "payment_needed.txt",
48
+ "decoded_bill.json",
49
+ "cashier_payload.json",
50
+ "state.json",
51
+ "pay_headers.txt",
52
+ "pay_status.txt",
53
+ "pay_response.json",
54
+ "payment_proof_body.json",
55
+ "payment_proof_header.txt",
56
+ "final_response.txt",
57
+ "final_headers.txt",
58
+ "final_status.txt",
59
+ }
60
+ KNOWN_DELIVERY_FAILURE_CODES = {
61
+ "CREATE_ORDER_ERROR",
62
+ "FULFILLMENT_CONFIRM_FAILED",
63
+ "FULFILLMENT_ERROR",
64
+ "INVALID_PAYMENT_PROOF",
65
+ "INVALID_PAYMENT_PROOF_FORMAT",
66
+ "MISSING_OUT_TRADE_NO",
67
+ "MISSING_RESOURCE_ID",
68
+ "ORDER_NOT_FOUND",
69
+ "RESOURCE_ID_MISMATCH",
70
+ "SIGN_ERROR",
71
+ "VERIFY_FAILED",
72
+ }
73
+ REDACTED = "<redacted>"
74
+ SENSITIVE_KEY_PATTERNS = (
75
+ "signature",
76
+ "payment_proof",
77
+ "paymentproof",
78
+ "proof_header",
79
+ "proofheader",
80
+ )
81
+ ACTIVE_ARTIFACT_DIRS: set[Path] = set()
82
+
83
+
84
+ def now_ms() -> str:
85
+ return str(int(time.time() * 1000))
86
+
87
+
88
+ def secure_write_text(path: Path, value: str) -> None:
89
+ if path.name not in ARTIFACT_FILES and path.parent.name.startswith(ARTIFACT_PREFIX):
90
+ raise RuntimeError(f"拒绝写入未登记的过程产物文件:{path.name}")
91
+ if path.exists() and path.is_symlink():
92
+ raise RuntimeError(f"过程产物文件禁止使用符号链接:{path}")
93
+ flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
94
+ if hasattr(os, "O_NOFOLLOW"):
95
+ flags |= os.O_NOFOLLOW
96
+ descriptor = os.open(path, flags, 0o600)
97
+ try:
98
+ if hasattr(os, "fchmod"):
99
+ os.fchmod(descriptor, 0o600)
100
+ with os.fdopen(descriptor, "w", encoding="utf-8", closefd=False) as stream:
101
+ stream.write(value)
102
+ stream.flush()
103
+ os.fsync(stream.fileno())
104
+ finally:
105
+ os.close(descriptor)
106
+
107
+
108
+ def secure_read_text(path: Path) -> str:
109
+ info = path.lstat()
110
+ if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode):
111
+ raise RuntimeError(f"过程产物必须是普通文件且不能是符号链接:{path}")
112
+ if os.name != "nt" and info.st_mode & 0o077:
113
+ raise RuntimeError(f"过程产物文件权限过宽,拒绝读取:{path}")
114
+ return path.read_text(encoding="utf-8")
115
+
116
+
117
+ def validate_artifact_dir(artifact_dir: Path) -> None:
118
+ if artifact_dir not in ACTIVE_ARTIFACT_DIRS:
119
+ raise RuntimeError("过程产物不属于本次命令,拒绝读取或清理")
120
+ info = artifact_dir.lstat()
121
+ if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
122
+ raise RuntimeError(f"过程产物路径必须是普通目录且不能是符号链接:{artifact_dir}")
123
+ if hasattr(os, "getuid") and info.st_uid != os.getuid():
124
+ raise RuntimeError(f"过程产物目录不属于当前用户:{artifact_dir}")
125
+ if os.name != "nt" and info.st_mode & 0o077:
126
+ raise RuntimeError(f"过程产物目录权限过宽,拒绝使用:{artifact_dir}")
127
+
128
+
129
+ def create_artifact_dir() -> Path:
130
+ artifact_dir = Path(tempfile.mkdtemp(prefix=ARTIFACT_PREFIX))
131
+ os.chmod(artifact_dir, 0o700)
132
+ ACTIVE_ARTIFACT_DIRS.add(artifact_dir)
133
+ return artifact_dir
134
+
135
+
136
+ def cleanup_artifact_dir(path: str | Path) -> None:
137
+ artifact_dir = Path(path)
138
+ validate_artifact_dir(artifact_dir)
139
+ children = list(artifact_dir.iterdir())
140
+ unsafe = [child.name for child in children if child.name not in ARTIFACT_FILES or child.is_symlink() or child.is_dir()]
141
+ if unsafe:
142
+ raise RuntimeError(f"过程产物目录包含未登记内容,拒绝自动删除:{', '.join(sorted(unsafe))}")
143
+ for child in children:
144
+ child.unlink()
145
+ artifact_dir.rmdir()
146
+ ACTIVE_ARTIFACT_DIRS.discard(artifact_dir)
147
+
148
+
149
+ def cleanup_active_artifacts() -> None:
150
+ for artifact_dir in list(ACTIVE_ARTIFACT_DIRS):
151
+ try:
152
+ cleanup_artifact_dir(artifact_dir)
153
+ except (OSError, RuntimeError, ValueError):
154
+ continue
155
+
156
+
157
+ def ensure_curl() -> None:
158
+ if not shutil.which("curl"):
159
+ raise SystemExit("需要 curl,但当前 PATH 中未找到 curl")
160
+
161
+
162
+ def run_curl(args: list[str]) -> str:
163
+ ensure_curl()
164
+ result = subprocess.run(
165
+ ["curl", *args],
166
+ stdout=subprocess.PIPE,
167
+ stderr=subprocess.STDOUT,
168
+ text=True,
169
+ check=False,
170
+ )
171
+ if result.returncode != 0:
172
+ raise RuntimeError(f"curl 执行失败,退出码 {result.returncode}:\n{result.stdout}")
173
+ return result.stdout
174
+
175
+
176
+ def prompt_missing(value: str | None, label: str, *, secret: bool = False) -> str:
177
+ if value:
178
+ return value
179
+ if secret:
180
+ import getpass
181
+
182
+ entered = getpass.getpass(f"{label}: ").strip()
183
+ else:
184
+ entered = input(f"{label}: ").strip()
185
+ if not entered:
186
+ raise SystemExit(f"缺少必填值:{label}")
187
+ return entered
188
+
189
+
190
+ def load_body(args: argparse.Namespace, method: str, default_body: str | None = None) -> str | None:
191
+ if args.body_file:
192
+ return Path(args.body_file).read_text(encoding="utf-8")
193
+ if args.body:
194
+ if args.body.startswith("@"):
195
+ return Path(args.body[1:]).read_text(encoding="utf-8")
196
+ return args.body
197
+ if default_body is not None:
198
+ return default_body
199
+ if method.upper() == "POST":
200
+ return prompt_missing(None, "POST 请求体 body")
201
+ return None
202
+
203
+
204
+ def parse_payment_needed(headers: str) -> str | None:
205
+ for line in headers.splitlines():
206
+ if line.lower().startswith("payment-needed:"):
207
+ return line.split(":", 1)[1].strip().strip('"')
208
+ compact = headers.strip()
209
+ if compact and "\n" not in compact and ":" not in compact:
210
+ return compact.strip('"')
211
+ return None
212
+
213
+
214
+ def parse_header_value(headers: str, name: str) -> str | None:
215
+ header_prefix = f"{name.lower()}:"
216
+ for line in headers.splitlines():
217
+ if line.lower().startswith(header_prefix):
218
+ return line.split(":", 1)[1].strip().strip('"')
219
+ return None
220
+
221
+
222
+ def fetch_payment_needed(
223
+ url: str,
224
+ method: str,
225
+ body: str | None,
226
+ content_type: str,
227
+ output_file: str | None,
228
+ require_status_402: bool = False,
229
+ ) -> str:
230
+ method = method.upper()
231
+ if method == "GET":
232
+ if require_status_402:
233
+ headers = run_curl(["-s", "-D", "-", "-o", "/dev/null", "-X", "GET", url])
234
+ payment_needed = parse_payment_needed(headers)
235
+ else:
236
+ headers = run_curl(["-s", "-I", url])
237
+ payment_needed = parse_payment_needed(headers)
238
+ if not payment_needed:
239
+ headers = run_curl(["-s", "-D", "-", "-o", "/dev/null", "-X", "GET", url])
240
+ payment_needed = parse_payment_needed(headers)
241
+ else:
242
+ curl_args = ["-s", "-D", "-", "-o", "/dev/null", "-X", method, url]
243
+ if body is not None:
244
+ curl_args.extend(["-H", f"Content-Type: {content_type}", "-d", body])
245
+ headers = run_curl(curl_args)
246
+ payment_needed = parse_payment_needed(headers)
247
+
248
+ if require_status_402:
249
+ statuses = [
250
+ parts[1]
251
+ for line in headers.splitlines()
252
+ if line.startswith("HTTP/") and len(parts := line.split()) > 1
253
+ ]
254
+ status = statuses[-1] if statuses else ""
255
+ if status != "402":
256
+ raise RuntimeError(f"HTTP 状态不是 402: {status or '未知'}")
257
+
258
+ if not payment_needed:
259
+ if require_status_402:
260
+ raise RuntimeError("未找到 Payment-Needed 响应头")
261
+ raise RuntimeError(f"未找到 Payment-Needed 响应头。原始响应头:\n{headers}")
262
+
263
+ if output_file:
264
+ secure_write_text(Path(output_file).expanduser().resolve(strict=False), payment_needed)
265
+ return payment_needed
266
+
267
+
268
+ def b64decode_json(value: str) -> dict[str, Any]:
269
+ compact = value.strip().strip('"')
270
+ padded = compact + ("=" * ((4 - len(compact) % 4) % 4))
271
+ try:
272
+ decoder = base64.urlsafe_b64decode if ("-" in compact or "_" in compact) else base64.b64decode
273
+ decoded = decoder(padded).decode("utf-8")
274
+ data = json.loads(decoded)
275
+ except Exception as exc: # noqa: BLE001 - 保留原始异常,便于定位账单解码问题。
276
+ raise ValueError(f"无法将 Payment-Needed 按 base64 解码为 JSON:{exc}") from exc
277
+ if not isinstance(data, dict):
278
+ raise ValueError("解码后的 Payment-Needed JSON 必须是对象")
279
+ return data
280
+
281
+
282
+ def validate_payment_needed_fields(data: dict[str, Any]) -> None:
283
+ missing = []
284
+ for parent, child in PAYMENT_NEEDED_REQUIRED_PATHS:
285
+ nested = data.get(parent)
286
+ if not isinstance(nested, dict) or nested.get(child) in (None, ""):
287
+ missing.append(f"{parent}.{child}")
288
+ if missing:
289
+ raise RuntimeError("Payment-Needed 缺少字段: " + ", ".join(missing))
290
+ method = data["method"]
291
+ if method.get("service_id") != A2M_SANDBOX_SERVICE_ID:
292
+ raise RuntimeError(
293
+ f"沙箱联调 method.service_id 必须为 {A2M_SANDBOX_SERVICE_ID},"
294
+ "请修正用户服务的沙箱运行配置后重试"
295
+ )
296
+
297
+
298
+ def snake_to_camel(key: str) -> str:
299
+ if "_" not in key:
300
+ return key
301
+ first, *rest = key.split("_")
302
+ return first + "".join(part[:1].upper() + part[1:] for part in rest if part)
303
+
304
+
305
+ def convert_keys(value: Any) -> Any:
306
+ if isinstance(value, list):
307
+ return [convert_keys(item) for item in value]
308
+ if isinstance(value, dict):
309
+ return {snake_to_camel(str(key)): convert_keys(item) for key, item in value.items()}
310
+ return value
311
+
312
+
313
+ def timestamp_from_out_trade_no(out_trade_no: str) -> str:
314
+ match = re.search(r"(\d{10,})$", out_trade_no or "")
315
+ return match.group(1) if match else now_ms()
316
+
317
+
318
+ def build_cashier_payload(decoded_bill: dict[str, Any], buyer_id: str, buyer_signature: str) -> dict[str, Any]:
319
+ converted = convert_keys(decoded_bill)
320
+ method = converted.get("method") if isinstance(converted.get("method"), dict) else {}
321
+ protocol = converted.get("protocol") if isinstance(converted.get("protocol"), dict) else {}
322
+ method = dict(method)
323
+ protocol = dict(protocol)
324
+
325
+ out_trade_no = str(protocol.get("outTradeNo") or "")
326
+ timestamp = timestamp_from_out_trade_no(out_trade_no)
327
+
328
+ method["buyerUniqueIdKey"] = "buyerExternalId"
329
+ protocol["buyerUniqueId"] = buyer_id
330
+
331
+ return {
332
+ "method": method,
333
+ "protocol": protocol,
334
+ "signature": {
335
+ "buyerExternalId": buyer_id,
336
+ "buyerSignature": buyer_signature,
337
+ "timestamp": timestamp,
338
+ },
339
+ }
340
+
341
+
342
+ def compact_json(value: Any) -> str:
343
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
344
+
345
+
346
+ def pretty_json(value: Any) -> str:
347
+ return json.dumps(value, ensure_ascii=False, indent=2)
348
+
349
+
350
+ def redact_for_display(value: Any) -> Any:
351
+ if isinstance(value, list):
352
+ return [redact_for_display(item) for item in value]
353
+ if isinstance(value, dict):
354
+ redacted: dict[str, Any] = {}
355
+ for key, item in value.items():
356
+ normalized = str(key).replace("-", "_").lower()
357
+ if any(pattern in normalized for pattern in SENSITIVE_KEY_PATTERNS):
358
+ redacted[str(key)] = REDACTED
359
+ else:
360
+ redacted[str(key)] = redact_for_display(item)
361
+ return redacted
362
+ return value
363
+
364
+
365
+ def post_json(url: str, payload: dict[str, Any]) -> tuple[int, str, dict[str, Any]]:
366
+ with tempfile.TemporaryDirectory() as tmp_dir:
367
+ headers_path = Path(tmp_dir) / "headers.txt"
368
+ body_path = Path(tmp_dir) / "body.txt"
369
+ status_text = run_curl(
370
+ [
371
+ "-s",
372
+ "-D",
373
+ str(headers_path),
374
+ "-o",
375
+ str(body_path),
376
+ "-w",
377
+ "%{http_code}",
378
+ "--connect-timeout",
379
+ "10",
380
+ "--max-time",
381
+ "60",
382
+ "-X",
383
+ "POST",
384
+ url,
385
+ "-H",
386
+ "Content-Type: application/json",
387
+ "-d",
388
+ compact_json(payload),
389
+ ],
390
+ ).strip()
391
+ try:
392
+ status_code = int(status_text)
393
+ except ValueError as exc:
394
+ raise RuntimeError(f"无法解析收银接口 HTTP 状态码:{status_text}") from exc
395
+ headers = headers_path.read_text(encoding="utf-8", errors="replace")
396
+ raw = body_path.read_text(encoding="utf-8", errors="replace")
397
+ try:
398
+ data = json.loads(raw)
399
+ except json.JSONDecodeError as exc:
400
+ raise RuntimeError(f"收银接口返回不是 JSON:\n{raw}") from exc
401
+ if not isinstance(data, dict):
402
+ raise RuntimeError(f"收银接口返回的 JSON 必须是对象:\n{raw}")
403
+ return status_code, headers, data
404
+
405
+
406
+ def is_retryable_pay_response(pay_response: dict[str, Any]) -> bool:
407
+ error_code = str(pay_response.get("errorCode") or "")
408
+ error_message = str(pay_response.get("errorMessage") or "")
409
+ return (
410
+ error_code in RETRYABLE_PAY_ERROR_CODES
411
+ or "系统繁忙" in error_message
412
+ or "稍后重试" in error_message
413
+ )
414
+
415
+
416
+ def request_cashier_with_retry(
417
+ url: str,
418
+ payload: dict[str, Any],
419
+ attempts: int,
420
+ delay_seconds: float,
421
+ ) -> tuple[int, str, dict[str, Any]]:
422
+ attempts = max(1, attempts)
423
+ last_result: tuple[int, str, dict[str, Any]] | None = None
424
+ for attempt in range(1, attempts + 1):
425
+ try:
426
+ result = post_json(url, payload)
427
+ except RuntimeError as exc:
428
+ if attempt < attempts:
429
+ print(f"\n收银接口请求异常,第 {attempt}/{attempts} 次:{exc},{delay_seconds} 秒后重试...")
430
+ time.sleep(delay_seconds)
431
+ continue
432
+ raise
433
+
434
+ status_code, headers, pay_response = result
435
+ last_result = result
436
+ if pay_response.get("payScheme"):
437
+ return result
438
+ if attempt < attempts and is_retryable_pay_response(pay_response):
439
+ error_code = pay_response.get("errorCode")
440
+ error_message = pay_response.get("errorMessage")
441
+ print(
442
+ f"\n收银接口返回临时错误,第 {attempt}/{attempts} 次:"
443
+ f"HTTP {status_code} {error_code} {error_message},{delay_seconds} 秒后重试..."
444
+ )
445
+ time.sleep(delay_seconds)
446
+ continue
447
+ return result
448
+
449
+ if last_result is None:
450
+ raise RuntimeError("收银接口未返回有效响应")
451
+ return last_result
452
+
453
+
454
+ def pay_url_from_response(pay_response: dict[str, Any]) -> str:
455
+ pay_scheme = pay_response.get("payScheme")
456
+ if not pay_scheme:
457
+ raise RuntimeError(f"收银接口返回中缺少 payScheme:\n{pretty_json(pay_response)}")
458
+ return PAY_URL_PREFIX + quote(str(pay_scheme), safe="")
459
+
460
+
461
+ def trade_no_from_response(pay_response: dict[str, Any]) -> str:
462
+ protocol = pay_response.get("protocol") if isinstance(pay_response.get("protocol"), dict) else {}
463
+ trade_no = protocol.get("tradeNo") or protocol.get("tradeCoreTradeNo")
464
+ if not trade_no:
465
+ raise RuntimeError(f"收银接口返回中缺少 tradeNo:\n{pretty_json(pay_response)}")
466
+ return str(trade_no)
467
+
468
+
469
+ def b64_json(value: dict[str, Any]) -> str:
470
+ return base64.b64encode(compact_json(value).encode("utf-8")).decode("ascii")
471
+
472
+
473
+ def build_payment_proof_header(
474
+ buyer_id: str,
475
+ trade_no: str,
476
+ payment_proof: str | None,
477
+ buyer_signature: str,
478
+ ) -> tuple[dict[str, Any], str]:
479
+ timestamp = now_ms()
480
+ proof = payment_proof or hashlib.sha256(f"{trade_no}|{buyer_id}|{timestamp}".encode("utf-8")).hexdigest()
481
+ client_session = b64_json(
482
+ {
483
+ "externalId": buyer_id,
484
+ "signature": buyer_signature,
485
+ "timestamp": timestamp,
486
+ },
487
+ )
488
+ proof_body = {
489
+ "protocol": {
490
+ "payment_proof": proof,
491
+ "trade_no": trade_no,
492
+ },
493
+ "method": {
494
+ "client_session": client_session,
495
+ },
496
+ }
497
+ return proof_body, b64_json(proof_body)
498
+
499
+
500
+ def retry_original_service(
501
+ url: str,
502
+ method: str,
503
+ body: str | None,
504
+ content_type: str,
505
+ proof_header: str,
506
+ ) -> tuple[int, str, str]:
507
+ with tempfile.TemporaryDirectory() as tmp_dir:
508
+ headers_path = Path(tmp_dir) / "headers.txt"
509
+ body_path = Path(tmp_dir) / "body.txt"
510
+ args = [
511
+ "-s",
512
+ "-D",
513
+ str(headers_path),
514
+ "-o",
515
+ str(body_path),
516
+ "-w",
517
+ "%{http_code}",
518
+ "--connect-timeout",
519
+ "10",
520
+ "--max-time",
521
+ "60",
522
+ "-X",
523
+ method.upper(),
524
+ url,
525
+ "-H",
526
+ f"Payment-Proof: {proof_header}",
527
+ ]
528
+ if body is not None:
529
+ args.extend(["-H", f"Content-Type: {content_type}", "-d", body])
530
+ status_text = run_curl(args).strip()
531
+ try:
532
+ status_code = int(status_text)
533
+ except ValueError as exc:
534
+ raise RuntimeError(f"无法解析最终服务 HTTP 状态码:{status_text}") from exc
535
+ headers = headers_path.read_text(encoding="utf-8", errors="replace")
536
+ response_body = body_path.read_text(encoding="utf-8", errors="replace")
537
+ return status_code, headers, response_body
538
+
539
+
540
+ def b64url_decode_json(value: str) -> dict[str, Any]:
541
+ compact = value.strip().strip('"')
542
+ padded = compact + ("=" * ((4 - len(compact) % 4) % 4))
543
+ try:
544
+ decoded = base64.urlsafe_b64decode(padded).decode("utf-8")
545
+ result = json.loads(decoded)
546
+ except Exception as exc: # noqa: BLE001 - 对无效响应头统一返回确定错误。
547
+ raise ValueError(f"Payment-Validation 不是合法的 Base64URL JSON:{exc}") from exc
548
+ if not isinstance(result, dict):
549
+ raise ValueError("Payment-Validation 解码结果必须是 JSON 对象")
550
+ return result
551
+
552
+
553
+ def find_delivery_failure(value: Any) -> str | None:
554
+ if isinstance(value, list):
555
+ for item in value:
556
+ failure = find_delivery_failure(item)
557
+ if failure:
558
+ return failure
559
+ return None
560
+ if not isinstance(value, dict):
561
+ return None
562
+
563
+ if value.get("success") is False:
564
+ return "success=false"
565
+ if value.get("validated") is False:
566
+ return "validated=false"
567
+ if value.get("fulfillment_confirmed") is False or value.get("fulfillmentConfirmed") is False:
568
+ return "fulfillment_confirmed=false"
569
+
570
+ for key in ("code", "errorCode", "sub_code", "subCode"):
571
+ code = str(value.get(key) or "")
572
+ if code in KNOWN_DELIVERY_FAILURE_CODES:
573
+ return code
574
+ for item in value.values():
575
+ failure = find_delivery_failure(item)
576
+ if failure:
577
+ return failure
578
+ return None
579
+
580
+
581
+ def nonempty_resource(value: Any) -> bool:
582
+ return value not in (None, "", [], {})
583
+
584
+
585
+ def validate_delivery_response(
586
+ status_code: int,
587
+ response_headers: str,
588
+ final_response: str,
589
+ require_payment_validation: bool,
590
+ ) -> tuple[bool, str]:
591
+ if status_code != 200:
592
+ return False, f"最终服务必须返回 HTTP 200,实际为 {status_code}"
593
+ if not final_response.strip():
594
+ return False, "最终服务返回空响应,未证明资源交付"
595
+
596
+ response_json: Any = None
597
+ try:
598
+ response_json = json.loads(final_response)
599
+ except json.JSONDecodeError:
600
+ response_json = None
601
+
602
+ response_resource_id = ""
603
+ if response_json is not None:
604
+ failure = find_delivery_failure(response_json)
605
+ if failure:
606
+ return False, f"最终服务返回明确业务失败:{failure}"
607
+ if not isinstance(response_json, dict):
608
+ return False, "最终服务 JSON 响应必须是对象,无法确认资源归属"
609
+ response_resource_id = str(response_json.get("resource_id") or response_json.get("resourceId") or "")
610
+ content = response_json.get("content")
611
+ if not response_resource_id or not nonempty_resource(content):
612
+ return False, "最终服务响应缺少非空 resource_id/resourceId 或 content"
613
+
614
+ payment_validation_raw = parse_header_value(response_headers, "Payment-Validation")
615
+ if require_payment_validation and not payment_validation_raw:
616
+ return False, "目标服务要求 Payment-Validation,但最终响应未返回该响应头"
617
+ if payment_validation_raw:
618
+ try:
619
+ payment_validation = b64url_decode_json(payment_validation_raw)
620
+ except ValueError as exc:
621
+ return False, str(exc)
622
+ validation_failure = find_delivery_failure(payment_validation)
623
+ if validation_failure:
624
+ return False, f"Payment-Validation 返回明确失败:{validation_failure}"
625
+ if payment_validation.get("validated") is not True:
626
+ return False, "Payment-Validation 未明确返回 validated=true"
627
+ validation_trade_no = str(payment_validation.get("trade_no") or payment_validation.get("tradeNo") or "")
628
+ validation_resource_id = str(
629
+ payment_validation.get("resource_id") or payment_validation.get("resourceId") or ""
630
+ )
631
+ if not validation_trade_no or not validation_resource_id:
632
+ return False, "Payment-Validation 缺少非空 trade_no/tradeNo 或 resource_id/resourceId"
633
+ if response_resource_id and validation_resource_id != response_resource_id:
634
+ return False, "Payment-Validation 与响应体的 resource_id 不一致"
635
+
636
+ if response_json is None and not payment_validation_raw:
637
+ return False, "非 JSON 资源响应缺少可用于确认归属的 Payment-Validation"
638
+ return True, "HTTP 200、非空可归属资源且无明确业务失败"
639
+
640
+
641
+ def write_json(path: Path, value: Any) -> None:
642
+ secure_write_text(path, pretty_json(value) + "\n")
643
+
644
+
645
+ def command_precheck(args: argparse.Namespace) -> int:
646
+ url = prompt_missing(args.url, "本地服务请求地址")
647
+ method = (args.method or "GET").upper()
648
+ if method not in {"GET", "POST"}:
649
+ raise SystemExit("当前技能只支持 GET 和 POST")
650
+ content_type = args.content_type or "application/json"
651
+ body = load_body(args, method, None)
652
+ decoded_bill = b64decode_json(
653
+ fetch_payment_needed(url, method, body, content_type, None, require_status_402=True)
654
+ )
655
+ validate_payment_needed_fields(decoded_bill)
656
+ fields = ", ".join(f"{parent}.{child}" for parent, child in PAYMENT_NEEDED_REQUIRED_PATHS)
657
+ print("Payment-Needed 预检通过,已确认 HTTP 402 和关键字段存在。")
658
+ print("已校验字段: " + fields)
659
+ return 0
660
+
661
+
662
+ def command_run(args: argparse.Namespace) -> int:
663
+ if not args.auto_complete:
664
+ raise RuntimeError("当前流程只支持一次连续执行 run --auto-complete;不保存跨轮付款或恢复状态")
665
+ url = prompt_missing(args.url, "本地服务请求地址")
666
+ method = (args.method or "GET").upper()
667
+ if method not in {"GET", "POST"}:
668
+ raise SystemExit("当前技能只支持 GET 和 POST")
669
+ buyer_id = prompt_missing(args.buyer_id, "沙箱买家 2088 账号")
670
+ content_type = args.content_type or "application/json"
671
+ body = load_body(args, method, None)
672
+ artifact_dir = create_artifact_dir()
673
+
674
+ try:
675
+ payment_needed = fetch_payment_needed(url, method, body, content_type, None)
676
+
677
+ decoded_bill = b64decode_json(payment_needed)
678
+ method_data = decoded_bill.get("method") if isinstance(decoded_bill.get("method"), dict) else {}
679
+ if method_data.get("service_id") != A2M_SANDBOX_SERVICE_ID:
680
+ raise RuntimeError(
681
+ f"沙箱联调 method.service_id 必须为 {A2M_SANDBOX_SERVICE_ID},"
682
+ "请修正用户服务的沙箱运行配置后重试"
683
+ )
684
+ cashier_payload = build_cashier_payload(decoded_bill, buyer_id, args.buyer_signature)
685
+ except (OSError, RuntimeError, ValueError):
686
+ cleanup_artifact_dir(artifact_dir)
687
+ raise
688
+
689
+ secure_write_text(artifact_dir / "payment_needed.txt", payment_needed)
690
+ write_json(artifact_dir / "decoded_bill.json", decoded_bill)
691
+ write_json(artifact_dir / "cashier_payload.json", cashier_payload)
692
+
693
+ state = {
694
+ "url": url,
695
+ "method": method,
696
+ "body": body,
697
+ "contentType": content_type,
698
+ "buyerId": buyer_id,
699
+ "buyerSignature": args.buyer_signature,
700
+ "payEndpoint": args.pay_endpoint,
701
+ "requirePaymentValidation": bool(args.require_payment_validation),
702
+ }
703
+ write_json(artifact_dir / "state.json", state)
704
+
705
+ print("Payment-Needed 已获取并保存到本地过程产物,不在终端输出原始值。")
706
+ print("\n解码后的 Payment-Needed JSON(已脱敏):")
707
+ print(pretty_json(redact_for_display(decoded_bill)))
708
+ print("\n收银接口请求体(已脱敏):")
709
+ print(pretty_json(redact_for_display(cashier_payload)))
710
+
711
+ pay_status, pay_headers, pay_response = request_cashier_with_retry(
712
+ args.pay_endpoint,
713
+ cashier_payload,
714
+ args.pay_retries,
715
+ args.pay_retry_delay,
716
+ )
717
+ state["payStatus"] = pay_status
718
+ state["payResponse"] = pay_response
719
+ secure_write_text(artifact_dir / "pay_headers.txt", pay_headers)
720
+ secure_write_text(artifact_dir / "pay_status.txt", str(pay_status))
721
+ write_json(artifact_dir / "pay_response.json", pay_response)
722
+ print("\n收银接口 HTTP 状态:")
723
+ print(pay_status)
724
+ print("\n收银接口返回:")
725
+ print(pretty_json(pay_response))
726
+
727
+ if not pay_response.get("payScheme"):
728
+ state["payError"] = {
729
+ "errorCode": pay_response.get("errorCode"),
730
+ "errorMessage": pay_response.get("errorMessage"),
731
+ }
732
+ write_json(artifact_dir / "state.json", state)
733
+ if not is_retryable_pay_response(pay_response):
734
+ cleanup_artifact_dir(artifact_dir)
735
+ raise RuntimeError("收银接口未返回 payScheme,且不是已确认的临时错误;敏感过程产物已清理")
736
+ raise RuntimeError(
737
+ "收银接口未返回 payScheme,无法生成付款链接。"
738
+ "本轮敏感过程产物将立即清理;请稍后重新执行完整联调。"
739
+ )
740
+
741
+ pay_url = pay_url_from_response(pay_response)
742
+ try:
743
+ trade_no = trade_no_from_response(pay_response)
744
+ except RuntimeError:
745
+ cleanup_artifact_dir(artifact_dir)
746
+ raise
747
+ state["payUrl"] = pay_url
748
+ state["tradeNo"] = trade_no
749
+ write_json(artifact_dir / "state.json", state)
750
+
751
+ if args.auto_complete:
752
+ print("\n继续执行 Payment-Proof 服务端联调...")
753
+ complete_args = argparse.Namespace(
754
+ artifact_dir=str(artifact_dir),
755
+ payment_proof=args.payment_proof,
756
+ buyer_signature=args.buyer_signature,
757
+ require_payment_validation=args.require_payment_validation,
758
+ )
759
+ result = command_complete(complete_args)
760
+ if result == 0:
761
+ print("\nAI 按量付费沙箱服务端联调通过。")
762
+ print("\n沙箱付款体验链接(可选):")
763
+ print(pay_url)
764
+ return result
765
+
766
+ raise RuntimeError("未进入自动完成分支")
767
+
768
+
769
+ def command_complete(args: argparse.Namespace) -> int:
770
+ artifact_dir = Path(prompt_missing(args.artifact_dir, "过程产物目录"))
771
+ validate_artifact_dir(artifact_dir)
772
+ state_path = artifact_dir / "state.json"
773
+ if not state_path.exists():
774
+ cleanup_artifact_dir(artifact_dir)
775
+ raise RuntimeError(f"缺少状态文件,无法恢复且已清理过程产物:{state_path}")
776
+ try:
777
+ state = json.loads(secure_read_text(state_path))
778
+ except json.JSONDecodeError as exc:
779
+ cleanup_artifact_dir(artifact_dir)
780
+ raise RuntimeError("状态文件不是合法 JSON,无法恢复且已清理过程产物") from exc
781
+ if not isinstance(state, dict):
782
+ cleanup_artifact_dir(artifact_dir)
783
+ raise RuntimeError(f"状态文件必须是 JSON 对象,无法恢复且已清理过程产物:{state_path}")
784
+ missing_keys = [key for key in ("url", "method", "buyerId", "tradeNo") if not state.get(key)]
785
+ if missing_keys:
786
+ cleanup_artifact_dir(artifact_dir)
787
+ raise RuntimeError(f"状态文件缺少必要字段,无法恢复且已清理过程产物:{', '.join(missing_keys)}")
788
+ buyer_signature = args.buyer_signature if args.buyer_signature is not None else state.get("buyerSignature", "-")
789
+ proof_body, proof_header = build_payment_proof_header(
790
+ buyer_id=str(state["buyerId"]),
791
+ trade_no=str(state["tradeNo"]),
792
+ payment_proof=args.payment_proof,
793
+ buyer_signature=str(buyer_signature),
794
+ )
795
+
796
+ write_json(artifact_dir / "payment_proof_body.json", proof_body)
797
+ secure_write_text(artifact_dir / "payment_proof_header.txt", proof_header)
798
+
799
+ print("Payment-Proof 请求体(已脱敏):")
800
+ print(pretty_json(redact_for_display(proof_body)))
801
+ print("\nPayment-Proof 请求头值已生成并保存到本地过程产物,不在终端输出原始值。")
802
+
803
+ status_code, response_headers, final_response = retry_original_service(
804
+ url=str(state["url"]),
805
+ method=str(state["method"]),
806
+ body=state.get("body"),
807
+ content_type=str(state.get("contentType") or "application/json"),
808
+ proof_header=proof_header,
809
+ )
810
+ secure_write_text(artifact_dir / "final_response.txt", final_response)
811
+ secure_write_text(artifact_dir / "final_headers.txt", response_headers)
812
+ secure_write_text(artifact_dir / "final_status.txt", str(status_code))
813
+
814
+ print("\n最终服务 HTTP 状态:")
815
+ print(status_code)
816
+ payment_validation = parse_header_value(response_headers, "Payment-Validation")
817
+ if payment_validation:
818
+ print("\nPayment-Validation 响应头已收到,原始值不在终端输出。")
819
+ print("\n最终服务响应体:")
820
+ print(final_response)
821
+ require_payment_validation = bool(
822
+ getattr(args, "require_payment_validation", False) or state.get("requirePaymentValidation")
823
+ )
824
+ passed, reason = validate_delivery_response(
825
+ status_code,
826
+ response_headers,
827
+ final_response,
828
+ require_payment_validation,
829
+ )
830
+ if not passed:
831
+ print(f"\nPayment-Proof 重试未通过:{reason}。")
832
+ print("请优先检查服务端订单映射、Payment-Proof 验证、资源防串和履约确认逻辑。")
833
+ return 1
834
+ print(f"\n资源交付证据校验通过:{reason}。")
835
+ cleanup_artifact_dir(artifact_dir)
836
+ print("敏感过程产物已安全清理。")
837
+ return 0
838
+
839
+
840
+ def build_parser() -> argparse.ArgumentParser:
841
+ parser = argparse.ArgumentParser(description=__doc__)
842
+ subparsers = parser.add_subparsers(dest="command")
843
+
844
+ precheck_parser = subparsers.add_parser("precheck", help="只读检查 Payment-Needed 的 HTTP 402 和关键字段")
845
+ precheck_parser.add_argument("--url", help="本地服务请求地址,GET 参数需要直接拼在 URL 中")
846
+ precheck_parser.add_argument("--method", help="HTTP 方法:GET 或 POST")
847
+ precheck_parser.add_argument("--body", help="POST 请求体,或用 @file 从文件读取请求体")
848
+ precheck_parser.add_argument("--body-file", help="保存 POST 请求体的文件")
849
+ precheck_parser.add_argument("--content-type", help="POST 请求的 Content-Type")
850
+ precheck_parser.set_defaults(func=command_precheck)
851
+
852
+ run_parser = subparsers.add_parser("run", help="一笔完成 Payment-Needed、沙箱收银和资源交付联调")
853
+ run_parser.add_argument("--url", help="本地服务请求地址,GET 参数需要直接拼在 URL 中")
854
+ run_parser.add_argument("--method", help="HTTP 方法:GET 或 POST")
855
+ run_parser.add_argument("--body", help="POST 请求体,或用 @file 从文件读取请求体")
856
+ run_parser.add_argument("--body-file", help="保存 POST 请求体的文件")
857
+ run_parser.add_argument("--content-type", help="POST 请求的 Content-Type")
858
+ run_parser.add_argument("--buyer-id", help="沙箱买家 2088 账号")
859
+ run_parser.add_argument("--buyer-signature", default="-", help="买家签名占位值")
860
+ run_parser.add_argument("--pay-retries", type=int, default=DEFAULT_PAY_RETRIES, help="沙箱收银接口临时失败时的重试次数")
861
+ run_parser.add_argument(
862
+ "--pay-retry-delay",
863
+ type=float,
864
+ default=DEFAULT_PAY_RETRY_DELAY_SECONDS,
865
+ help="沙箱收银接口重试间隔秒数",
866
+ )
867
+ run_parser.add_argument(
868
+ "--auto-complete",
869
+ action="store_true",
870
+ help="生成付款链接后连续携带 Payment-Proof 重试原始服务",
871
+ )
872
+ run_parser.add_argument(
873
+ "--require-payment-validation",
874
+ action="store_true",
875
+ help="目标服务实现 Payment-Validation 时要求并校验该响应头",
876
+ )
877
+ run_parser.set_defaults(func=command_run, pay_endpoint=PAY_ENDPOINT, payment_proof=None)
878
+
879
+ return parser
880
+
881
+
882
+ def main(argv: list[str]) -> int:
883
+ if argv and argv[0] not in {"precheck", "run", "-h", "--help"}:
884
+ argv = ["run", *argv]
885
+ parser = build_parser()
886
+ args = parser.parse_args(argv)
887
+ if not hasattr(args, "func"):
888
+ parser.print_help()
889
+ return 2
890
+ try:
891
+ return args.func(args)
892
+ except (RuntimeError, ValueError) as exc:
893
+ print(f"执行失败:{exc}", file=sys.stderr)
894
+ return 1
895
+ finally:
896
+ cleanup_active_artifacts()
897
+
898
+
899
+ if __name__ == "__main__":
900
+ raise SystemExit(main(sys.argv[1:]))