@alipay/alipay-aipay 1.0.0 → 1.0.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 (117) hide show
  1. package/bin/alipay-aipay.js +6 -0
  2. package/dist/commands/install.d.ts +11 -0
  3. package/dist/commands/install.d.ts.map +1 -0
  4. package/dist/commands/install.js +182 -0
  5. package/dist/commands/install.js.map +1 -0
  6. package/dist/commands/prepare-tools.d.ts +5 -0
  7. package/dist/commands/prepare-tools.d.ts.map +1 -0
  8. package/dist/commands/prepare-tools.js +29 -0
  9. package/dist/commands/prepare-tools.js.map +1 -0
  10. package/dist/commands/uninstall.d.ts +10 -0
  11. package/dist/commands/uninstall.d.ts.map +1 -0
  12. package/dist/commands/uninstall.js +82 -0
  13. package/dist/commands/uninstall.js.map +1 -0
  14. package/dist/commands/update.d.ts +10 -0
  15. package/dist/commands/update.d.ts.map +1 -0
  16. package/dist/commands/update.js +155 -0
  17. package/dist/commands/update.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +65 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/types.d.ts +43 -0
  23. package/dist/types.d.ts.map +1 -0
  24. package/dist/types.js +2 -0
  25. package/dist/types.js.map +1 -0
  26. package/dist/utils/alipay-cli.d.ts +15 -0
  27. package/dist/utils/alipay-cli.d.ts.map +1 -0
  28. package/dist/utils/alipay-cli.js +221 -0
  29. package/dist/utils/alipay-cli.js.map +1 -0
  30. package/dist/utils/config.d.ts +48 -0
  31. package/dist/utils/config.d.ts.map +1 -0
  32. package/dist/utils/config.js +207 -0
  33. package/dist/utils/config.js.map +1 -0
  34. package/dist/utils/file.d.ts +55 -0
  35. package/dist/utils/file.d.ts.map +1 -0
  36. package/dist/utils/file.js +154 -0
  37. package/dist/utils/file.js.map +1 -0
  38. package/dist/utils/gpg.d.ts +14 -0
  39. package/dist/utils/gpg.d.ts.map +1 -0
  40. package/dist/utils/gpg.js +247 -0
  41. package/dist/utils/gpg.js.map +1 -0
  42. package/dist/utils/jq.d.ts +14 -0
  43. package/dist/utils/jq.d.ts.map +1 -0
  44. package/dist/utils/jq.js +221 -0
  45. package/dist/utils/jq.js.map +1 -0
  46. package/dist/utils/npm.d.ts +17 -0
  47. package/dist/utils/npm.d.ts.map +1 -0
  48. package/dist/utils/npm.js +42 -0
  49. package/dist/utils/npm.js.map +1 -0
  50. package/dist/utils/shell.d.ts +11 -0
  51. package/dist/utils/shell.d.ts.map +1 -0
  52. package/dist/utils/shell.js +54 -0
  53. package/dist/utils/shell.js.map +1 -0
  54. package/package.json +48 -7
  55. package/skill/SKILL.md +118 -0
  56. package/skill/references/integration/flow.md +487 -0
  57. package/skill/references/integration/modules/alipay-sdk-reminder.md +256 -0
  58. package/skill/references/integration/modules/checklist.md +122 -0
  59. package/skill/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
  60. package/skill/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 +77 -0
  61. package/skill/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 +74 -0
  62. package/skill/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
  63. package/skill/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 +58 -0
  64. package/skill/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 +128 -0
  65. package/skill/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.cs +500 -0
  66. package/skill/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
  67. package/skill/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 +80 -0
  68. package/skill/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 +78 -0
  69. package/skill/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
  70. package/skill/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 +66 -0
  71. package/skill/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 +135 -0
  72. package/skill/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.java +610 -0
  73. package/skill/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
  74. package/skill/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 +57 -0
  75. package/skill/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 +55 -0
  76. package/skill/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
  77. package/skill/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 +77 -0
  78. package/skill/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 +65 -0
  79. package/skill/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.js +461 -0
  80. package/skill/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
  81. package/skill/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 +67 -0
  82. package/skill/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 +66 -0
  83. package/skill/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
  84. package/skill/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 +56 -0
  85. package/skill/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 +107 -0
  86. package/skill/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.php +444 -0
  87. package/skill/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
  88. package/skill/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 +65 -0
  89. package/skill/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 +63 -0
  90. package/skill/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
  91. package/skill/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 +48 -0
  92. package/skill/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 +43 -0
  93. package/skill/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.py +465 -0
  94. package/skill/references/integration/modules/interface-guide.md +90 -0
  95. package/skill/references/integration/modules/product-decision.md +90 -0
  96. package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +270 -0
  97. package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +133 -0
  98. package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +218 -0
  99. package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +659 -0
  100. package/skill/references/normal/alipay-cli-env.md +214 -0
  101. package/skill/references/normal/rejection-guide.md +37 -0
  102. package/skill/references/normal/scripts/common.sh +32 -0
  103. package/skill/references/normal/scripts/detect_dev_tool.sh +46 -0
  104. package/skill/references/onboarding/flow.md +573 -0
  105. package/skill/references/onboarding/modules/app-release.md +415 -0
  106. package/skill/references/onboarding/modules/authorization.md +269 -0
  107. package/skill/references/onboarding/modules/error-handling.md +251 -0
  108. package/skill/references/onboarding/modules/mcc-reference.md +209 -0
  109. package/skill/references/onboarding/modules/product-sign.md +304 -0
  110. package/skill/references/onboarding/modules/scripts/app.sh +705 -0
  111. package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +248 -0
  112. package/skill/references/onboarding/modules/scripts/auth.sh +505 -0
  113. package/skill/references/onboarding/modules/scripts/error_handler.sh +452 -0
  114. package/skill/references/onboarding/modules/scripts/query_sign_status.sh +146 -0
  115. package/skill/references/onboarding/modules/scripts/service.sh +267 -0
  116. package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +152 -0
  117. package/skill/references/onboarding/modules/service-registration.md +294 -0
@@ -0,0 +1,659 @@
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 re
11
+ import shlex
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any
19
+ from urllib.parse import quote
20
+
21
+
22
+ PAY_ENDPOINT = "http://aicashier.dl.alipaydev.com/openclawpay/agent/v1/pay"
23
+ PAY_URL_PREFIX = "https://render.alipay.com/p/yuyan/180020010001290755/pay.html?schema="
24
+ DEFAULT_PAYMENT_NEEDED_FILE = "/tmp/402_needed_file.txt"
25
+ DEFAULT_PAY_RETRIES = 3
26
+ DEFAULT_PAY_RETRY_DELAY_SECONDS = 2.0
27
+ RETRYABLE_PAY_ERROR_CODES = {"PAY_SUBMIT_FAILED"}
28
+ REDACTED = "<redacted>"
29
+ SENSITIVE_KEY_PATTERNS = (
30
+ "signature",
31
+ "payment_proof",
32
+ "paymentproof",
33
+ "proof_header",
34
+ "proofheader",
35
+ )
36
+
37
+
38
+ def now_ms() -> str:
39
+ return str(int(time.time() * 1000))
40
+
41
+
42
+ def ensure_curl() -> None:
43
+ if not shutil.which("curl"):
44
+ raise SystemExit("需要 curl,但当前 PATH 中未找到 curl")
45
+
46
+
47
+ def run_curl(args: list[str]) -> str:
48
+ ensure_curl()
49
+ result = subprocess.run(
50
+ ["curl", *args],
51
+ stdout=subprocess.PIPE,
52
+ stderr=subprocess.STDOUT,
53
+ text=True,
54
+ check=False,
55
+ )
56
+ if result.returncode != 0:
57
+ raise RuntimeError(f"curl 执行失败,退出码 {result.returncode}:\n{result.stdout}")
58
+ return result.stdout
59
+
60
+
61
+ def prompt_missing(value: str | None, label: str, *, secret: bool = False) -> str:
62
+ if value:
63
+ return value
64
+ if secret:
65
+ import getpass
66
+
67
+ entered = getpass.getpass(f"{label}: ").strip()
68
+ else:
69
+ entered = input(f"{label}: ").strip()
70
+ if not entered:
71
+ raise SystemExit(f"缺少必填值:{label}")
72
+ return entered
73
+
74
+
75
+ def load_body(args: argparse.Namespace, method: str, default_body: str | None = None) -> str | None:
76
+ if args.body_file:
77
+ return Path(args.body_file).read_text(encoding="utf-8")
78
+ if args.body:
79
+ if args.body.startswith("@"):
80
+ return Path(args.body[1:]).read_text(encoding="utf-8")
81
+ return args.body
82
+ if default_body is not None:
83
+ return default_body
84
+ if method.upper() == "POST":
85
+ return prompt_missing(None, "POST 请求体 body")
86
+ return None
87
+
88
+
89
+ def parse_payment_needed(headers: str) -> str | None:
90
+ for line in headers.splitlines():
91
+ if line.lower().startswith("payment-needed:"):
92
+ return line.split(":", 1)[1].strip().strip('"')
93
+ compact = headers.strip()
94
+ if compact and "\n" not in compact and ":" not in compact:
95
+ return compact.strip('"')
96
+ return None
97
+
98
+
99
+ def parse_header_value(headers: str, name: str) -> str | None:
100
+ header_prefix = f"{name.lower()}:"
101
+ for line in headers.splitlines():
102
+ if line.lower().startswith(header_prefix):
103
+ return line.split(":", 1)[1].strip().strip('"')
104
+ return None
105
+
106
+
107
+ def fetch_payment_needed(
108
+ url: str,
109
+ method: str,
110
+ body: str | None,
111
+ content_type: str,
112
+ output_file: str,
113
+ ) -> str:
114
+ method = method.upper()
115
+ if method == "GET":
116
+ headers = run_curl(["-s", "-I", url])
117
+ payment_needed = parse_payment_needed(headers)
118
+ if not payment_needed:
119
+ headers = run_curl(["-s", "-D", "-", "-o", "/dev/null", "-X", "GET", url])
120
+ payment_needed = parse_payment_needed(headers)
121
+ else:
122
+ curl_args = ["-s", "-D", "-", "-o", "/dev/null", "-X", method, url]
123
+ if body is not None:
124
+ curl_args.extend(["-H", f"Content-Type: {content_type}", "-d", body])
125
+ headers = run_curl(curl_args)
126
+ payment_needed = parse_payment_needed(headers)
127
+
128
+ if not payment_needed:
129
+ raise RuntimeError(f"未找到 Payment-Needed 响应头。原始响应头:\n{headers}")
130
+
131
+ Path(output_file).write_text(payment_needed, encoding="utf-8")
132
+ return payment_needed
133
+
134
+
135
+ def b64decode_json(value: str) -> dict[str, Any]:
136
+ compact = value.strip().strip('"')
137
+ padded = compact + ("=" * ((4 - len(compact) % 4) % 4))
138
+ try:
139
+ decoder = base64.urlsafe_b64decode if ("-" in compact or "_" in compact) else base64.b64decode
140
+ decoded = decoder(padded).decode("utf-8")
141
+ data = json.loads(decoded)
142
+ except Exception as exc: # noqa: BLE001 - 保留原始异常,便于定位账单解码问题。
143
+ raise ValueError(f"无法将 Payment-Needed 按 base64 解码为 JSON:{exc}") from exc
144
+ if not isinstance(data, dict):
145
+ raise ValueError("解码后的 Payment-Needed JSON 必须是对象")
146
+ return data
147
+
148
+
149
+ def snake_to_camel(key: str) -> str:
150
+ if "_" not in key:
151
+ return key
152
+ first, *rest = key.split("_")
153
+ return first + "".join(part[:1].upper() + part[1:] for part in rest if part)
154
+
155
+
156
+ def convert_keys(value: Any) -> Any:
157
+ if isinstance(value, list):
158
+ return [convert_keys(item) for item in value]
159
+ if isinstance(value, dict):
160
+ return {snake_to_camel(str(key)): convert_keys(item) for key, item in value.items()}
161
+ return value
162
+
163
+
164
+ def timestamp_from_out_trade_no(out_trade_no: str) -> str:
165
+ match = re.search(r"(\d{10,})$", out_trade_no or "")
166
+ return match.group(1) if match else now_ms()
167
+
168
+
169
+ def build_cashier_payload(decoded_bill: dict[str, Any], buyer_id: str, buyer_signature: str) -> dict[str, Any]:
170
+ converted = convert_keys(decoded_bill)
171
+ method = converted.get("method") if isinstance(converted.get("method"), dict) else {}
172
+ protocol = converted.get("protocol") if isinstance(converted.get("protocol"), dict) else {}
173
+ method = dict(method)
174
+ protocol = dict(protocol)
175
+
176
+ out_trade_no = str(protocol.get("outTradeNo") or "")
177
+ timestamp = timestamp_from_out_trade_no(out_trade_no)
178
+
179
+ method["buyerUniqueIdKey"] = "buyerExternalId"
180
+ protocol["buyerUniqueId"] = buyer_id
181
+
182
+ return {
183
+ "method": method,
184
+ "protocol": protocol,
185
+ "signature": {
186
+ "buyerExternalId": buyer_id,
187
+ "buyerSignature": buyer_signature,
188
+ "timestamp": timestamp,
189
+ },
190
+ }
191
+
192
+
193
+ def compact_json(value: Any) -> str:
194
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
195
+
196
+
197
+ def pretty_json(value: Any) -> str:
198
+ return json.dumps(value, ensure_ascii=False, indent=2)
199
+
200
+
201
+ def redact_for_display(value: Any) -> Any:
202
+ if isinstance(value, list):
203
+ return [redact_for_display(item) for item in value]
204
+ if isinstance(value, dict):
205
+ redacted: dict[str, Any] = {}
206
+ for key, item in value.items():
207
+ normalized = str(key).replace("-", "_").lower()
208
+ if any(pattern in normalized for pattern in SENSITIVE_KEY_PATTERNS):
209
+ redacted[str(key)] = REDACTED
210
+ else:
211
+ redacted[str(key)] = redact_for_display(item)
212
+ return redacted
213
+ return value
214
+
215
+
216
+ def shell_quote(value: str | Path) -> str:
217
+ return shlex.quote(str(value))
218
+
219
+
220
+ def post_json(url: str, payload: dict[str, Any]) -> tuple[int, str, dict[str, Any]]:
221
+ with tempfile.TemporaryDirectory() as tmp_dir:
222
+ headers_path = Path(tmp_dir) / "headers.txt"
223
+ body_path = Path(tmp_dir) / "body.txt"
224
+ status_text = run_curl(
225
+ [
226
+ "-s",
227
+ "-D",
228
+ str(headers_path),
229
+ "-o",
230
+ str(body_path),
231
+ "-w",
232
+ "%{http_code}",
233
+ "--connect-timeout",
234
+ "10",
235
+ "--max-time",
236
+ "60",
237
+ "-X",
238
+ "POST",
239
+ url,
240
+ "-H",
241
+ "Content-Type: application/json",
242
+ "-d",
243
+ compact_json(payload),
244
+ ],
245
+ ).strip()
246
+ try:
247
+ status_code = int(status_text)
248
+ except ValueError as exc:
249
+ raise RuntimeError(f"无法解析收银接口 HTTP 状态码:{status_text}") from exc
250
+ headers = headers_path.read_text(encoding="utf-8", errors="replace")
251
+ raw = body_path.read_text(encoding="utf-8", errors="replace")
252
+ try:
253
+ data = json.loads(raw)
254
+ except json.JSONDecodeError as exc:
255
+ raise RuntimeError(f"收银接口返回不是 JSON:\n{raw}") from exc
256
+ if not isinstance(data, dict):
257
+ raise RuntimeError(f"收银接口返回的 JSON 必须是对象:\n{raw}")
258
+ return status_code, headers, data
259
+
260
+
261
+ def is_retryable_pay_response(pay_response: dict[str, Any]) -> bool:
262
+ error_code = str(pay_response.get("errorCode") or "")
263
+ error_message = str(pay_response.get("errorMessage") or "")
264
+ return (
265
+ error_code in RETRYABLE_PAY_ERROR_CODES
266
+ or "系统繁忙" in error_message
267
+ or "稍后重试" in error_message
268
+ )
269
+
270
+
271
+ def request_cashier_with_retry(
272
+ url: str,
273
+ payload: dict[str, Any],
274
+ attempts: int,
275
+ delay_seconds: float,
276
+ ) -> tuple[int, str, dict[str, Any]]:
277
+ attempts = max(1, attempts)
278
+ last_result: tuple[int, str, dict[str, Any]] | None = None
279
+ for attempt in range(1, attempts + 1):
280
+ try:
281
+ result = post_json(url, payload)
282
+ except RuntimeError as exc:
283
+ if attempt < attempts:
284
+ print(f"\n收银接口请求异常,第 {attempt}/{attempts} 次:{exc},{delay_seconds} 秒后重试...")
285
+ time.sleep(delay_seconds)
286
+ continue
287
+ raise
288
+
289
+ status_code, headers, pay_response = result
290
+ last_result = result
291
+ if pay_response.get("payScheme"):
292
+ return result
293
+ if attempt < attempts and is_retryable_pay_response(pay_response):
294
+ error_code = pay_response.get("errorCode")
295
+ error_message = pay_response.get("errorMessage")
296
+ print(
297
+ f"\n收银接口返回临时错误,第 {attempt}/{attempts} 次:"
298
+ f"HTTP {status_code} {error_code} {error_message},{delay_seconds} 秒后重试..."
299
+ )
300
+ time.sleep(delay_seconds)
301
+ continue
302
+ return result
303
+
304
+ if last_result is None:
305
+ raise RuntimeError("收银接口未返回有效响应")
306
+ return last_result
307
+
308
+
309
+ def pay_url_from_response(pay_response: dict[str, Any]) -> str:
310
+ pay_scheme = pay_response.get("payScheme")
311
+ if not pay_scheme:
312
+ raise RuntimeError(f"收银接口返回中缺少 payScheme:\n{pretty_json(pay_response)}")
313
+ return PAY_URL_PREFIX + quote(str(pay_scheme), safe="")
314
+
315
+
316
+ def trade_no_from_response(pay_response: dict[str, Any]) -> str:
317
+ protocol = pay_response.get("protocol") if isinstance(pay_response.get("protocol"), dict) else {}
318
+ trade_no = protocol.get("tradeNo") or protocol.get("tradeCoreTradeNo")
319
+ if not trade_no:
320
+ raise RuntimeError(f"收银接口返回中缺少 tradeNo:\n{pretty_json(pay_response)}")
321
+ return str(trade_no)
322
+
323
+
324
+ def b64_json(value: dict[str, Any]) -> str:
325
+ return base64.b64encode(compact_json(value).encode("utf-8")).decode("ascii")
326
+
327
+
328
+ def build_payment_proof_header(
329
+ buyer_id: str,
330
+ trade_no: str,
331
+ payment_proof: str | None,
332
+ buyer_signature: str,
333
+ ) -> tuple[dict[str, Any], str]:
334
+ timestamp = now_ms()
335
+ proof = payment_proof or hashlib.sha256(f"{trade_no}|{buyer_id}|{timestamp}".encode("utf-8")).hexdigest()
336
+ client_session = b64_json(
337
+ {
338
+ "externalId": buyer_id,
339
+ "signature": buyer_signature,
340
+ "timestamp": timestamp,
341
+ },
342
+ )
343
+ proof_body = {
344
+ "protocol": {
345
+ "payment_proof": proof,
346
+ "trade_no": trade_no,
347
+ },
348
+ "method": {
349
+ "client_session": client_session,
350
+ },
351
+ }
352
+ return proof_body, b64_json(proof_body)
353
+
354
+
355
+ def retry_original_service(
356
+ url: str,
357
+ method: str,
358
+ body: str | None,
359
+ content_type: str,
360
+ proof_header: str,
361
+ ) -> tuple[int, str, str]:
362
+ with tempfile.TemporaryDirectory() as tmp_dir:
363
+ headers_path = Path(tmp_dir) / "headers.txt"
364
+ body_path = Path(tmp_dir) / "body.txt"
365
+ args = [
366
+ "-s",
367
+ "-D",
368
+ str(headers_path),
369
+ "-o",
370
+ str(body_path),
371
+ "-w",
372
+ "%{http_code}",
373
+ "--connect-timeout",
374
+ "10",
375
+ "--max-time",
376
+ "60",
377
+ "-X",
378
+ method.upper(),
379
+ url,
380
+ "-H",
381
+ f"Payment-Proof: {proof_header}",
382
+ ]
383
+ if body is not None:
384
+ args.extend(["-H", f"Content-Type: {content_type}", "-d", body])
385
+ status_text = run_curl(args).strip()
386
+ try:
387
+ status_code = int(status_text)
388
+ except ValueError as exc:
389
+ raise RuntimeError(f"无法解析最终服务 HTTP 状态码:{status_text}") from exc
390
+ headers = headers_path.read_text(encoding="utf-8", errors="replace")
391
+ response_body = body_path.read_text(encoding="utf-8", errors="replace")
392
+ return status_code, headers, response_body
393
+
394
+
395
+ def write_json(path: Path, value: Any) -> None:
396
+ path.write_text(pretty_json(value) + "\n", encoding="utf-8")
397
+
398
+
399
+ def create_artifact_dir(path: str | None) -> Path:
400
+ artifact_dir = Path(path or f"/tmp/alipay_local_402_sandbox_pay_{now_ms()}")
401
+ if artifact_dir.exists() and not artifact_dir.is_dir():
402
+ raise RuntimeError(f"过程产物路径已存在但不是目录:{artifact_dir}")
403
+ artifact_dir.mkdir(parents=True, exist_ok=True)
404
+ return artifact_dir
405
+
406
+
407
+ def load_reuse_artifact(path: str | None) -> tuple[dict[str, Any], str | None]:
408
+ if not path:
409
+ return {}, None
410
+ artifact_dir = Path(path)
411
+ if not artifact_dir.is_dir():
412
+ raise RuntimeError(f"--reuse-artifact 指定的产物目录不存在:{artifact_dir}")
413
+ state_path = artifact_dir / "state.json"
414
+ payment_needed_path = artifact_dir / "payment_needed.txt"
415
+ if not state_path.exists():
416
+ raise RuntimeError(f"--reuse-artifact 缺少 state.json:{state_path}")
417
+ if not payment_needed_path.exists():
418
+ raise RuntimeError(f"--reuse-artifact 缺少 payment_needed.txt:{payment_needed_path}")
419
+
420
+ loaded = json.loads(state_path.read_text(encoding="utf-8"))
421
+ if not isinstance(loaded, dict):
422
+ raise RuntimeError(f"--reuse-artifact 的 state.json 必须是 JSON 对象:{state_path}")
423
+ state = loaded
424
+ required_keys = ["url", "method", "buyerId"]
425
+ missing_keys = [key for key in required_keys if not state.get(key)]
426
+ if missing_keys:
427
+ raise RuntimeError(
428
+ f"--reuse-artifact 的 state.json 缺少必要字段:{', '.join(missing_keys)}。"
429
+ "请使用同一脚本成功执行过 run 后再复用产物。"
430
+ )
431
+
432
+ payment_needed = payment_needed_path.read_text(encoding="utf-8").strip()
433
+ if not payment_needed:
434
+ raise RuntimeError(f"--reuse-artifact 的 payment_needed.txt 为空:{payment_needed_path}")
435
+ return state, payment_needed
436
+
437
+
438
+ def command_run(args: argparse.Namespace) -> int:
439
+ reuse_state, reuse_payment_needed = load_reuse_artifact(args.reuse_artifact)
440
+ url = prompt_missing(args.url or reuse_state.get("url"), "本地服务请求地址")
441
+ method = (args.method or reuse_state.get("method") or "GET").upper()
442
+ if method not in {"GET", "POST"}:
443
+ raise SystemExit("当前技能只支持 GET 和 POST")
444
+ buyer_id = prompt_missing(args.buyer_id or reuse_state.get("buyerId"), "沙箱买家 2088 账号")
445
+ content_type = args.content_type or str(reuse_state.get("contentType") or "application/json")
446
+ body = load_body(args, method, reuse_state.get("body"))
447
+ artifact_dir = create_artifact_dir(args.artifact_dir)
448
+
449
+ if args.payment_needed:
450
+ payment_needed = args.payment_needed.strip()
451
+ Path(args.payment_needed_file).write_text(payment_needed, encoding="utf-8")
452
+ elif reuse_payment_needed:
453
+ payment_needed = reuse_payment_needed
454
+ Path(args.payment_needed_file).write_text(payment_needed, encoding="utf-8")
455
+ else:
456
+ payment_needed = fetch_payment_needed(
457
+ url,
458
+ method,
459
+ body,
460
+ content_type,
461
+ args.payment_needed_file,
462
+ )
463
+
464
+ decoded_bill = b64decode_json(payment_needed)
465
+ cashier_payload = build_cashier_payload(decoded_bill, buyer_id, args.buyer_signature)
466
+
467
+ (artifact_dir / "payment_needed.txt").write_text(payment_needed, encoding="utf-8")
468
+ write_json(artifact_dir / "decoded_bill.json", decoded_bill)
469
+ write_json(artifact_dir / "cashier_payload.json", cashier_payload)
470
+
471
+ state = {
472
+ "url": url,
473
+ "method": method,
474
+ "body": body,
475
+ "contentType": content_type,
476
+ "buyerId": buyer_id,
477
+ "buyerSignature": args.buyer_signature,
478
+ "paymentNeededFile": args.payment_needed_file,
479
+ "payEndpoint": args.pay_endpoint,
480
+ "reusedArtifact": args.reuse_artifact,
481
+ }
482
+
483
+ print("Payment-Needed 已获取并保存到本地过程产物,不在终端输出原始值。")
484
+ print("\n解码后的 Payment-Needed JSON(已脱敏):")
485
+ print(pretty_json(redact_for_display(decoded_bill)))
486
+ print("\n收银接口请求体(已脱敏):")
487
+ print(pretty_json(redact_for_display(cashier_payload)))
488
+
489
+ if args.dry_run:
490
+ state["dryRun"] = True
491
+ write_json(artifact_dir / "state.json", state)
492
+ print(f"\ndry-run 完成。过程产物目录:{artifact_dir}")
493
+ return 0
494
+
495
+ pay_status, pay_headers, pay_response = request_cashier_with_retry(
496
+ args.pay_endpoint,
497
+ cashier_payload,
498
+ args.pay_retries,
499
+ args.pay_retry_delay,
500
+ )
501
+ state["payStatus"] = pay_status
502
+ state["payResponse"] = pay_response
503
+ (artifact_dir / "pay_headers.txt").write_text(pay_headers, encoding="utf-8")
504
+ (artifact_dir / "pay_status.txt").write_text(str(pay_status), encoding="utf-8")
505
+ write_json(artifact_dir / "pay_response.json", pay_response)
506
+ print("\n收银接口 HTTP 状态:")
507
+ print(pay_status)
508
+ print("\n收银接口返回:")
509
+ print(pretty_json(pay_response))
510
+
511
+ if not pay_response.get("payScheme"):
512
+ state["payError"] = {
513
+ "errorCode": pay_response.get("errorCode"),
514
+ "errorMessage": pay_response.get("errorMessage"),
515
+ }
516
+ write_json(artifact_dir / "state.json", state)
517
+ raise RuntimeError(
518
+ "收银接口未返回 payScheme,无法生成付款链接。"
519
+ f"过程产物已保存:{artifact_dir}。"
520
+ "如果是 PAY_SUBMIT_FAILED/系统繁忙,可稍后复用同一个 Payment-Needed 重试:\n"
521
+ f"python3 {shell_quote(Path(__file__).resolve())} run "
522
+ f"--reuse-artifact {shell_quote(artifact_dir)}"
523
+ )
524
+
525
+ pay_url = pay_url_from_response(pay_response)
526
+ trade_no = trade_no_from_response(pay_response)
527
+ state["payUrl"] = pay_url
528
+ state["tradeNo"] = trade_no
529
+ write_json(artifact_dir / "state.json", state)
530
+
531
+ print("\n请在浏览器打开以下链接,并使用沙箱买家账号完成付款:")
532
+ print(pay_url)
533
+ print(f"\n过程产物目录:{artifact_dir}")
534
+ print("付款成功后运行:")
535
+ print(f"python3 {Path(__file__).resolve()} complete --artifact-dir {artifact_dir}")
536
+
537
+ if args.wait:
538
+ input("\n沙箱付款成功后按回车,继续重试原始服务...")
539
+ complete_args = argparse.Namespace(
540
+ artifact_dir=str(artifact_dir),
541
+ payment_proof=args.payment_proof,
542
+ buyer_signature=args.buyer_signature,
543
+ )
544
+ return command_complete(complete_args)
545
+
546
+ return 0
547
+
548
+
549
+ def command_complete(args: argparse.Namespace) -> int:
550
+ artifact_dir = Path(prompt_missing(args.artifact_dir, "过程产物目录"))
551
+ state_path = artifact_dir / "state.json"
552
+ if not state_path.exists():
553
+ raise SystemExit(f"缺少状态文件:{state_path}")
554
+ state = json.loads(state_path.read_text(encoding="utf-8"))
555
+ if not isinstance(state, dict):
556
+ raise SystemExit(f"状态文件必须是 JSON 对象:{state_path}")
557
+ missing_keys = [key for key in ("url", "method", "buyerId", "tradeNo") if not state.get(key)]
558
+ if missing_keys:
559
+ raise SystemExit(f"状态文件缺少必要字段:{', '.join(missing_keys)}")
560
+
561
+ buyer_signature = args.buyer_signature if args.buyer_signature is not None else state.get("buyerSignature", "-")
562
+ proof_body, proof_header = build_payment_proof_header(
563
+ buyer_id=str(state["buyerId"]),
564
+ trade_no=str(state["tradeNo"]),
565
+ payment_proof=args.payment_proof,
566
+ buyer_signature=str(buyer_signature),
567
+ )
568
+
569
+ write_json(artifact_dir / "payment_proof_body.json", proof_body)
570
+ (artifact_dir / "payment_proof_header.txt").write_text(proof_header, encoding="utf-8")
571
+
572
+ print("Payment-Proof 请求体(已脱敏):")
573
+ print(pretty_json(redact_for_display(proof_body)))
574
+ print("\nPayment-Proof 请求头值已生成并保存到本地过程产物,不在终端输出原始值。")
575
+
576
+ status_code, response_headers, final_response = retry_original_service(
577
+ url=str(state["url"]),
578
+ method=str(state["method"]),
579
+ body=state.get("body"),
580
+ content_type=str(state.get("contentType") or "application/json"),
581
+ proof_header=proof_header,
582
+ )
583
+ (artifact_dir / "final_response.txt").write_text(final_response, encoding="utf-8")
584
+ (artifact_dir / "final_headers.txt").write_text(response_headers, encoding="utf-8")
585
+ (artifact_dir / "final_status.txt").write_text(str(status_code), encoding="utf-8")
586
+
587
+ print("\n最终服务 HTTP 状态:")
588
+ print(status_code)
589
+ payment_validation = parse_header_value(response_headers, "Payment-Validation")
590
+ if payment_validation:
591
+ print("\nPayment-Validation 响应头:")
592
+ print(payment_validation)
593
+ print("\n最终服务响应体:")
594
+ print(final_response)
595
+ print(f"\n过程产物目录:{artifact_dir}")
596
+ if status_code < 200 or status_code >= 300:
597
+ print(
598
+ f"\nPayment-Proof 重试未通过:最终服务返回 HTTP {status_code}。"
599
+ "请优先检查服务端订单映射、Payment-Proof 验证、资源防串和履约确认逻辑。"
600
+ )
601
+ return 1
602
+ return 0
603
+
604
+
605
+ def build_parser() -> argparse.ArgumentParser:
606
+ parser = argparse.ArgumentParser(description=__doc__)
607
+ subparsers = parser.add_subparsers(dest="command")
608
+
609
+ run_parser = subparsers.add_parser("run", help="获取 Payment-Needed 并生成沙箱付款链接")
610
+ run_parser.add_argument("--url", help="本地服务请求地址,GET 参数需要直接拼在 URL 中")
611
+ run_parser.add_argument("--method", help="HTTP 方法:GET 或 POST")
612
+ run_parser.add_argument("--body", help="POST 请求体,或用 @file 从文件读取请求体")
613
+ run_parser.add_argument("--body-file", help="保存 POST 请求体的文件")
614
+ run_parser.add_argument("--content-type", help="POST 请求的 Content-Type")
615
+ run_parser.add_argument("--buyer-id", help="沙箱买家 2088 账号")
616
+ run_parser.add_argument("--buyer-signature", default="-", help="买家签名占位值")
617
+ run_parser.add_argument("--payment-needed", help="已有的 Payment-Needed 响应头值")
618
+ run_parser.add_argument("--payment-needed-file", default=DEFAULT_PAYMENT_NEEDED_FILE)
619
+ run_parser.add_argument("--reuse-artifact", help="复用上一轮 run 产物中的 url/method/body/content-type/payment_needed")
620
+ run_parser.add_argument("--pay-endpoint", default=PAY_ENDPOINT)
621
+ run_parser.add_argument("--artifact-dir", help="生成过程产物的目录")
622
+ run_parser.add_argument("--payment-proof", help="使用 --wait 时可选的最终 payment_proof 值")
623
+ run_parser.add_argument("--pay-retries", type=int, default=DEFAULT_PAY_RETRIES, help="沙箱收银接口临时失败时的重试次数")
624
+ run_parser.add_argument(
625
+ "--pay-retry-delay",
626
+ type=float,
627
+ default=DEFAULT_PAY_RETRY_DELAY_SECONDS,
628
+ help="沙箱收银接口重试间隔秒数",
629
+ )
630
+ run_parser.add_argument("--dry-run", action="store_true", help="调用收银接口前停止")
631
+ run_parser.add_argument("--wait", action="store_true", help="等待付款后立刻重试原始服务")
632
+ run_parser.set_defaults(func=command_run)
633
+
634
+ complete_parser = subparsers.add_parser("complete", help="携带 Payment-Proof 重试原始服务")
635
+ complete_parser.add_argument("--artifact-dir", required=True, help="run 命令生成的过程产物目录")
636
+ complete_parser.add_argument("--payment-proof", help="可选 payment_proof;默认生成一个确定性的非空哈希")
637
+ complete_parser.add_argument("--buyer-signature", help="买家签名占位值")
638
+ complete_parser.set_defaults(func=command_complete)
639
+
640
+ return parser
641
+
642
+
643
+ def main(argv: list[str]) -> int:
644
+ if argv and argv[0] not in {"run", "complete", "-h", "--help"}:
645
+ argv = ["run", *argv]
646
+ parser = build_parser()
647
+ args = parser.parse_args(argv)
648
+ if not hasattr(args, "func"):
649
+ parser.print_help()
650
+ return 2
651
+ try:
652
+ return args.func(args)
653
+ except (RuntimeError, ValueError) as exc:
654
+ print(f"执行失败:{exc}", file=sys.stderr)
655
+ return 1
656
+
657
+
658
+ if __name__ == "__main__":
659
+ raise SystemExit(main(sys.argv[1:]))