@alipay/alipay-aipay 1.2.2 → 1.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +13 -12
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +17 -9
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +5 -5
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/utils/config.d.ts +5 -3
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +34 -12
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/file.d.ts +8 -0
- package/dist/utils/file.d.ts.map +1 -1
- package/dist/utils/file.js +161 -9
- package/dist/utils/file.js.map +1 -1
- package/dist/utils/jq.d.ts.map +1 -1
- package/dist/utils/jq.js +8 -0
- package/dist/utils/jq.js.map +1 -1
- package/package.json +6 -5
- package/skill/SKILL.md +15 -169
- package/skill/VERSION +1 -1
- package/skill/references/integration/flow.md +321 -292
- package/skill/references/integration/modules/alipay-sdk-reminder.md +5 -3
- package/skill/references/integration/modules/checklist.md +41 -27
- package/skill/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
- 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 +8 -4
- 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 +6 -2
- package/skill/references/integration/modules/code-examples/csharp/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.cs +3 -1
- package/skill/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
- 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 +4 -1
- 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 +5 -2
- package/skill/references/integration/modules/code-examples/java/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemoController.java +3 -1
- package/skill/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
- 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 +16 -10
- 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 +10 -4
- package/skill/references/integration/modules/code-examples/nodejs/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.js +2 -1
- package/skill/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
- 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 +4 -1
- 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 +5 -2
- package/skill/references/integration/modules/code-examples/php/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.php +2 -1
- package/skill/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
- 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 +4 -1
- 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 +5 -1
- package/skill/references/integration/modules/code-examples/python/4-/346/214/211/351/207/217/344/273/230/350/264/271/A2MPaymentDemo.py +4 -2
- package/skill/references/integration/modules/interface-contract/aipay-interface-contract.md +53 -0
- package/skill/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
- package/skill/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
- package/skill/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
- package/skill/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
- package/skill/references/integration/modules/interface-guide.md +37 -72
- package/skill/references/integration/modules/product-decision.md +10 -27
- package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +62 -94
- package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +43 -40
- package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +93 -114
- package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +357 -147
- package/skill/references/integration/modules/scripts/sandbox_config.sh +299 -0
- package/skill/references/integration/modules/troubleshooting-flow.md +28 -0
- package/skill/references/normal/alipay-cli-env.md +15 -5
- package/skill/references/normal/full-process-routing.md +80 -0
- package/skill/references/normal/json/customer-messages.json +937 -0
- package/skill/references/normal/json/policy-rules.json +179 -0
- package/skill/references/normal/rejection-guide.md +11 -6
- package/skill/references/normal/scripts/alipay_cli_env.mjs +29 -0
- package/skill/references/normal/scripts/alipay_cli_env.sh +21 -0
- package/skill/references/normal/scripts/common.sh +87 -0
- package/skill/references/normal/scripts/detect_dev_tool.sh +5 -1
- package/skill/references/normal/scripts/integration_message_runner.mjs +801 -0
- package/skill/references/normal/scripts/onboarding_message_runner.mjs +436 -0
- package/skill/references/normal/scripts/open_official_url.sh +82 -0
- package/skill/references/normal/scripts/platform_compat.mjs +39 -0
- package/skill/references/normal/scripts/project_route_inspector.sh +132 -0
- package/skill/references/normal/scripts/python_script_runner.mjs +56 -0
- package/skill/references/normal/scripts/render_customer_message.mjs +1263 -0
- package/skill/references/normal/scripts/self_update.mjs +160 -0
- package/skill/references/normal/scripts/telemetry.mjs +1278 -0
- package/skill/references/normal/self-update.md +18 -51
- package/skill/references/onboarding/flow.md +381 -283
- package/skill/references/onboarding/modules/app-release.md +132 -179
- package/skill/references/onboarding/modules/authorization.md +53 -64
- package/skill/references/onboarding/modules/error-handling.md +31 -34
- package/skill/references/onboarding/modules/product-sign.md +62 -66
- package/skill/references/onboarding/modules/scripts/app.sh +397 -158
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +38 -22
- package/skill/references/onboarding/modules/scripts/auth.sh +479 -167
- package/skill/references/onboarding/modules/scripts/download_key_tool.sh +157 -0
- package/skill/references/onboarding/modules/scripts/error_handler.sh +100 -130
- package/skill/references/onboarding/modules/scripts/network_retry.sh +96 -0
- package/skill/references/onboarding/modules/scripts/query_sign_status.sh +17 -70
- package/skill/references/onboarding/modules/scripts/service.sh +153 -89
- package/skill/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +16 -14
- package/skill/references/onboarding/modules/service-registration.md +67 -72
- package/skill/references/runtime-entry.md +62 -0
|
@@ -7,9 +7,10 @@ import argparse
|
|
|
7
7
|
import base64
|
|
8
8
|
import hashlib
|
|
9
9
|
import json
|
|
10
|
+
import os
|
|
10
11
|
import re
|
|
11
|
-
import shlex
|
|
12
12
|
import shutil
|
|
13
|
+
import stat
|
|
13
14
|
import subprocess
|
|
14
15
|
import sys
|
|
15
16
|
import tempfile
|
|
@@ -22,10 +23,53 @@ from urllib.parse import quote
|
|
|
22
23
|
PAY_ENDPOINT = "http://aicashier.dl.alipaydev.com/openclawpay/agent/v1/pay"
|
|
23
24
|
PAY_URL_PREFIX = "https://render.alipay.com/p/yuyan/180020010001290755/pay.html?schema="
|
|
24
25
|
A2M_SANDBOX_SERVICE_ID = "api_mock_service_id"
|
|
25
|
-
|
|
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
|
+
)
|
|
26
42
|
DEFAULT_PAY_RETRIES = 3
|
|
27
43
|
DEFAULT_PAY_RETRY_DELAY_SECONDS = 2.0
|
|
28
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
|
+
}
|
|
29
73
|
REDACTED = "<redacted>"
|
|
30
74
|
SENSITIVE_KEY_PATTERNS = (
|
|
31
75
|
"signature",
|
|
@@ -34,12 +78,82 @@ SENSITIVE_KEY_PATTERNS = (
|
|
|
34
78
|
"proof_header",
|
|
35
79
|
"proofheader",
|
|
36
80
|
)
|
|
81
|
+
ACTIVE_ARTIFACT_DIRS: set[Path] = set()
|
|
37
82
|
|
|
38
83
|
|
|
39
84
|
def now_ms() -> str:
|
|
40
85
|
return str(int(time.time() * 1000))
|
|
41
86
|
|
|
42
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
|
+
|
|
43
157
|
def ensure_curl() -> None:
|
|
44
158
|
if not shutil.which("curl"):
|
|
45
159
|
raise SystemExit("需要 curl,但当前 PATH 中未找到 curl")
|
|
@@ -110,15 +224,20 @@ def fetch_payment_needed(
|
|
|
110
224
|
method: str,
|
|
111
225
|
body: str | None,
|
|
112
226
|
content_type: str,
|
|
113
|
-
output_file: str,
|
|
227
|
+
output_file: str | None,
|
|
228
|
+
require_status_402: bool = False,
|
|
114
229
|
) -> str:
|
|
115
230
|
method = method.upper()
|
|
116
231
|
if method == "GET":
|
|
117
|
-
|
|
118
|
-
payment_needed = parse_payment_needed(headers)
|
|
119
|
-
if not payment_needed:
|
|
232
|
+
if require_status_402:
|
|
120
233
|
headers = run_curl(["-s", "-D", "-", "-o", "/dev/null", "-X", "GET", url])
|
|
121
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)
|
|
122
241
|
else:
|
|
123
242
|
curl_args = ["-s", "-D", "-", "-o", "/dev/null", "-X", method, url]
|
|
124
243
|
if body is not None:
|
|
@@ -126,10 +245,23 @@ def fetch_payment_needed(
|
|
|
126
245
|
headers = run_curl(curl_args)
|
|
127
246
|
payment_needed = parse_payment_needed(headers)
|
|
128
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
|
+
|
|
129
258
|
if not payment_needed:
|
|
259
|
+
if require_status_402:
|
|
260
|
+
raise RuntimeError("未找到 Payment-Needed 响应头")
|
|
130
261
|
raise RuntimeError(f"未找到 Payment-Needed 响应头。原始响应头:\n{headers}")
|
|
131
262
|
|
|
132
|
-
|
|
263
|
+
if output_file:
|
|
264
|
+
secure_write_text(Path(output_file).expanduser().resolve(strict=False), payment_needed)
|
|
133
265
|
return payment_needed
|
|
134
266
|
|
|
135
267
|
|
|
@@ -147,6 +279,22 @@ def b64decode_json(value: str) -> dict[str, Any]:
|
|
|
147
279
|
return data
|
|
148
280
|
|
|
149
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
|
+
|
|
150
298
|
def snake_to_camel(key: str) -> str:
|
|
151
299
|
if "_" not in key:
|
|
152
300
|
return key
|
|
@@ -214,10 +362,6 @@ def redact_for_display(value: Any) -> Any:
|
|
|
214
362
|
return value
|
|
215
363
|
|
|
216
364
|
|
|
217
|
-
def shell_quote(value: str | Path) -> str:
|
|
218
|
-
return shlex.quote(str(value))
|
|
219
|
-
|
|
220
|
-
|
|
221
365
|
def post_json(url: str, payload: dict[str, Any]) -> tuple[int, str, dict[str, Any]]:
|
|
222
366
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
223
367
|
headers_path = Path(tmp_dir) / "headers.txt"
|
|
@@ -393,85 +537,156 @@ def retry_original_service(
|
|
|
393
537
|
return status_code, headers, response_body
|
|
394
538
|
|
|
395
539
|
|
|
396
|
-
def
|
|
397
|
-
|
|
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
|
|
398
551
|
|
|
399
552
|
|
|
400
|
-
def
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
406
579
|
|
|
407
580
|
|
|
408
|
-
def
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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 ""
|
|
431
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 不一致"
|
|
432
635
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
437
660
|
|
|
438
661
|
|
|
439
662
|
def command_run(args: argparse.Namespace) -> int:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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()
|
|
443
667
|
if method not in {"GET", "POST"}:
|
|
444
668
|
raise SystemExit("当前技能只支持 GET 和 POST")
|
|
445
|
-
buyer_id = prompt_missing(args.buyer_id
|
|
446
|
-
content_type = args.content_type or
|
|
447
|
-
body = load_body(args, method,
|
|
448
|
-
artifact_dir = create_artifact_dir(
|
|
449
|
-
|
|
450
|
-
if args.payment_needed:
|
|
451
|
-
payment_needed = args.payment_needed.strip()
|
|
452
|
-
Path(args.payment_needed_file).write_text(payment_needed, encoding="utf-8")
|
|
453
|
-
elif reuse_payment_needed:
|
|
454
|
-
payment_needed = reuse_payment_needed
|
|
455
|
-
Path(args.payment_needed_file).write_text(payment_needed, encoding="utf-8")
|
|
456
|
-
else:
|
|
457
|
-
payment_needed = fetch_payment_needed(
|
|
458
|
-
url,
|
|
459
|
-
method,
|
|
460
|
-
body,
|
|
461
|
-
content_type,
|
|
462
|
-
args.payment_needed_file,
|
|
463
|
-
)
|
|
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()
|
|
464
673
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
473
688
|
|
|
474
|
-
(artifact_dir / "payment_needed.txt"
|
|
689
|
+
secure_write_text(artifact_dir / "payment_needed.txt", payment_needed)
|
|
475
690
|
write_json(artifact_dir / "decoded_bill.json", decoded_bill)
|
|
476
691
|
write_json(artifact_dir / "cashier_payload.json", cashier_payload)
|
|
477
692
|
|
|
@@ -482,10 +697,10 @@ def command_run(args: argparse.Namespace) -> int:
|
|
|
482
697
|
"contentType": content_type,
|
|
483
698
|
"buyerId": buyer_id,
|
|
484
699
|
"buyerSignature": args.buyer_signature,
|
|
485
|
-
"paymentNeededFile": args.payment_needed_file,
|
|
486
700
|
"payEndpoint": args.pay_endpoint,
|
|
487
|
-
"
|
|
701
|
+
"requirePaymentValidation": bool(args.require_payment_validation),
|
|
488
702
|
}
|
|
703
|
+
write_json(artifact_dir / "state.json", state)
|
|
489
704
|
|
|
490
705
|
print("Payment-Needed 已获取并保存到本地过程产物,不在终端输出原始值。")
|
|
491
706
|
print("\n解码后的 Payment-Needed JSON(已脱敏):")
|
|
@@ -493,12 +708,6 @@ def command_run(args: argparse.Namespace) -> int:
|
|
|
493
708
|
print("\n收银接口请求体(已脱敏):")
|
|
494
709
|
print(pretty_json(redact_for_display(cashier_payload)))
|
|
495
710
|
|
|
496
|
-
if args.dry_run:
|
|
497
|
-
state["dryRun"] = True
|
|
498
|
-
write_json(artifact_dir / "state.json", state)
|
|
499
|
-
print(f"\ndry-run 完成。过程产物目录:{artifact_dir}")
|
|
500
|
-
return 0
|
|
501
|
-
|
|
502
711
|
pay_status, pay_headers, pay_response = request_cashier_with_retry(
|
|
503
712
|
args.pay_endpoint,
|
|
504
713
|
cashier_payload,
|
|
@@ -507,8 +716,8 @@ def command_run(args: argparse.Namespace) -> int:
|
|
|
507
716
|
)
|
|
508
717
|
state["payStatus"] = pay_status
|
|
509
718
|
state["payResponse"] = pay_response
|
|
510
|
-
(artifact_dir / "pay_headers.txt"
|
|
511
|
-
(artifact_dir / "pay_status.txt"
|
|
719
|
+
secure_write_text(artifact_dir / "pay_headers.txt", pay_headers)
|
|
720
|
+
secure_write_text(artifact_dir / "pay_status.txt", str(pay_status))
|
|
512
721
|
write_json(artifact_dir / "pay_response.json", pay_response)
|
|
513
722
|
print("\n收银接口 HTTP 状态:")
|
|
514
723
|
print(pay_status)
|
|
@@ -521,29 +730,31 @@ def command_run(args: argparse.Namespace) -> int:
|
|
|
521
730
|
"errorMessage": pay_response.get("errorMessage"),
|
|
522
731
|
}
|
|
523
732
|
write_json(artifact_dir / "state.json", state)
|
|
524
|
-
|
|
733
|
+
if not is_retryable_pay_response(pay_response):
|
|
734
|
+
cleanup_artifact_dir(artifact_dir)
|
|
735
|
+
raise RuntimeError("收银接口未返回 payScheme,且不是已确认的临时错误;敏感过程产物已清理")
|
|
525
736
|
raise RuntimeError(
|
|
526
737
|
"收银接口未返回 payScheme,无法生成付款链接。"
|
|
527
|
-
|
|
528
|
-
"如果是 PAY_SUBMIT_FAILED/系统繁忙,可稍后复用同一个 Payment-Needed 重试:\n"
|
|
529
|
-
f"python3 {shell_quote(Path(__file__).resolve())} run "
|
|
530
|
-
f"--reuse-artifact {shell_quote(artifact_dir)}{retry_mode}"
|
|
738
|
+
"本轮敏感过程产物将立即清理;请稍后重新执行完整联调。"
|
|
531
739
|
)
|
|
532
740
|
|
|
533
741
|
pay_url = pay_url_from_response(pay_response)
|
|
534
|
-
|
|
742
|
+
try:
|
|
743
|
+
trade_no = trade_no_from_response(pay_response)
|
|
744
|
+
except RuntimeError:
|
|
745
|
+
cleanup_artifact_dir(artifact_dir)
|
|
746
|
+
raise
|
|
535
747
|
state["payUrl"] = pay_url
|
|
536
748
|
state["tradeNo"] = trade_no
|
|
537
749
|
write_json(artifact_dir / "state.json", state)
|
|
538
750
|
|
|
539
|
-
print(f"\n过程产物目录:{artifact_dir}")
|
|
540
|
-
|
|
541
751
|
if args.auto_complete:
|
|
542
752
|
print("\n继续执行 Payment-Proof 服务端联调...")
|
|
543
753
|
complete_args = argparse.Namespace(
|
|
544
754
|
artifact_dir=str(artifact_dir),
|
|
545
755
|
payment_proof=args.payment_proof,
|
|
546
756
|
buyer_signature=args.buyer_signature,
|
|
757
|
+
require_payment_validation=args.require_payment_validation,
|
|
547
758
|
)
|
|
548
759
|
result = command_complete(complete_args)
|
|
549
760
|
if result == 0:
|
|
@@ -552,35 +763,28 @@ def command_run(args: argparse.Namespace) -> int:
|
|
|
552
763
|
print(pay_url)
|
|
553
764
|
return result
|
|
554
765
|
|
|
555
|
-
|
|
556
|
-
print(pay_url)
|
|
557
|
-
print("付款成功后运行:")
|
|
558
|
-
print(f"python3 {Path(__file__).resolve()} complete --artifact-dir {artifact_dir}")
|
|
559
|
-
|
|
560
|
-
if args.wait:
|
|
561
|
-
input("\n沙箱付款成功后按回车,继续重试原始服务...")
|
|
562
|
-
complete_args = argparse.Namespace(
|
|
563
|
-
artifact_dir=str(artifact_dir),
|
|
564
|
-
payment_proof=args.payment_proof,
|
|
565
|
-
buyer_signature=args.buyer_signature,
|
|
566
|
-
)
|
|
567
|
-
return command_complete(complete_args)
|
|
568
|
-
|
|
569
|
-
return 0
|
|
766
|
+
raise RuntimeError("未进入自动完成分支")
|
|
570
767
|
|
|
571
768
|
|
|
572
769
|
def command_complete(args: argparse.Namespace) -> int:
|
|
573
770
|
artifact_dir = Path(prompt_missing(args.artifact_dir, "过程产物目录"))
|
|
771
|
+
validate_artifact_dir(artifact_dir)
|
|
574
772
|
state_path = artifact_dir / "state.json"
|
|
575
773
|
if not state_path.exists():
|
|
576
|
-
|
|
577
|
-
|
|
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
|
|
578
781
|
if not isinstance(state, dict):
|
|
579
|
-
|
|
782
|
+
cleanup_artifact_dir(artifact_dir)
|
|
783
|
+
raise RuntimeError(f"状态文件必须是 JSON 对象,无法恢复且已清理过程产物:{state_path}")
|
|
580
784
|
missing_keys = [key for key in ("url", "method", "buyerId", "tradeNo") if not state.get(key)]
|
|
581
785
|
if missing_keys:
|
|
582
|
-
|
|
583
|
-
|
|
786
|
+
cleanup_artifact_dir(artifact_dir)
|
|
787
|
+
raise RuntimeError(f"状态文件缺少必要字段,无法恢复且已清理过程产物:{', '.join(missing_keys)}")
|
|
584
788
|
buyer_signature = args.buyer_signature if args.buyer_signature is not None else state.get("buyerSignature", "-")
|
|
585
789
|
proof_body, proof_header = build_payment_proof_header(
|
|
586
790
|
buyer_id=str(state["buyerId"]),
|
|
@@ -590,7 +794,7 @@ def command_complete(args: argparse.Namespace) -> int:
|
|
|
590
794
|
)
|
|
591
795
|
|
|
592
796
|
write_json(artifact_dir / "payment_proof_body.json", proof_body)
|
|
593
|
-
(artifact_dir / "payment_proof_header.txt"
|
|
797
|
+
secure_write_text(artifact_dir / "payment_proof_header.txt", proof_header)
|
|
594
798
|
|
|
595
799
|
print("Payment-Proof 请求体(已脱敏):")
|
|
596
800
|
print(pretty_json(redact_for_display(proof_body)))
|
|
@@ -603,25 +807,33 @@ def command_complete(args: argparse.Namespace) -> int:
|
|
|
603
807
|
content_type=str(state.get("contentType") or "application/json"),
|
|
604
808
|
proof_header=proof_header,
|
|
605
809
|
)
|
|
606
|
-
(artifact_dir / "final_response.txt"
|
|
607
|
-
(artifact_dir / "final_headers.txt"
|
|
608
|
-
(artifact_dir / "final_status.txt"
|
|
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))
|
|
609
813
|
|
|
610
814
|
print("\n最终服务 HTTP 状态:")
|
|
611
815
|
print(status_code)
|
|
612
816
|
payment_validation = parse_header_value(response_headers, "Payment-Validation")
|
|
613
817
|
if payment_validation:
|
|
614
|
-
print("\nPayment-Validation
|
|
615
|
-
print(payment_validation)
|
|
818
|
+
print("\nPayment-Validation 响应头已收到,原始值不在终端输出。")
|
|
616
819
|
print("\n最终服务响应体:")
|
|
617
820
|
print(final_response)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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 验证、资源防串和履约确认逻辑。")
|
|
624
833
|
return 1
|
|
834
|
+
print(f"\n资源交付证据校验通过:{reason}。")
|
|
835
|
+
cleanup_artifact_dir(artifact_dir)
|
|
836
|
+
print("敏感过程产物已安全清理。")
|
|
625
837
|
return 0
|
|
626
838
|
|
|
627
839
|
|
|
@@ -629,7 +841,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
629
841
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
630
842
|
subparsers = parser.add_subparsers(dest="command")
|
|
631
843
|
|
|
632
|
-
|
|
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、沙箱收银和资源交付联调")
|
|
633
853
|
run_parser.add_argument("--url", help="本地服务请求地址,GET 参数需要直接拼在 URL 中")
|
|
634
854
|
run_parser.add_argument("--method", help="HTTP 方法:GET 或 POST")
|
|
635
855
|
run_parser.add_argument("--body", help="POST 请求体,或用 @file 从文件读取请求体")
|
|
@@ -637,15 +857,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
637
857
|
run_parser.add_argument("--content-type", help="POST 请求的 Content-Type")
|
|
638
858
|
run_parser.add_argument("--buyer-id", help="沙箱买家 2088 账号")
|
|
639
859
|
run_parser.add_argument("--buyer-signature", default="-", help="买家签名占位值")
|
|
640
|
-
run_parser.add_argument("--payment-needed", help="已有的 Payment-Needed 响应头值")
|
|
641
|
-
run_parser.add_argument("--payment-needed-file", default=DEFAULT_PAYMENT_NEEDED_FILE)
|
|
642
|
-
run_parser.add_argument("--reuse-artifact", help="复用上一轮 run 产物中的 url/method/body/content-type/payment_needed")
|
|
643
|
-
run_parser.add_argument("--pay-endpoint", default=PAY_ENDPOINT)
|
|
644
|
-
run_parser.add_argument("--artifact-dir", help="生成过程产物的目录")
|
|
645
|
-
run_parser.add_argument(
|
|
646
|
-
"--payment-proof",
|
|
647
|
-
help="使用 --auto-complete 或 --wait 时可选的最终 payment_proof 值",
|
|
648
|
-
)
|
|
649
860
|
run_parser.add_argument("--pay-retries", type=int, default=DEFAULT_PAY_RETRIES, help="沙箱收银接口临时失败时的重试次数")
|
|
650
861
|
run_parser.add_argument(
|
|
651
862
|
"--pay-retry-delay",
|
|
@@ -653,26 +864,23 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
653
864
|
default=DEFAULT_PAY_RETRY_DELAY_SECONDS,
|
|
654
865
|
help="沙箱收银接口重试间隔秒数",
|
|
655
866
|
)
|
|
656
|
-
run_parser.add_argument("--dry-run", action="store_true", help="调用收银接口前停止")
|
|
657
867
|
run_parser.add_argument(
|
|
658
868
|
"--auto-complete",
|
|
659
869
|
action="store_true",
|
|
660
870
|
help="生成付款链接后连续携带 Payment-Proof 重试原始服务",
|
|
661
871
|
)
|
|
662
|
-
run_parser.add_argument(
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
complete_parser.add_argument("--buyer-signature", help="买家签名占位值")
|
|
669
|
-
complete_parser.set_defaults(func=command_complete)
|
|
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)
|
|
670
878
|
|
|
671
879
|
return parser
|
|
672
880
|
|
|
673
881
|
|
|
674
882
|
def main(argv: list[str]) -> int:
|
|
675
|
-
if argv and argv[0] not in {"
|
|
883
|
+
if argv and argv[0] not in {"precheck", "run", "-h", "--help"}:
|
|
676
884
|
argv = ["run", *argv]
|
|
677
885
|
parser = build_parser()
|
|
678
886
|
args = parser.parse_args(argv)
|
|
@@ -684,6 +892,8 @@ def main(argv: list[str]) -> int:
|
|
|
684
892
|
except (RuntimeError, ValueError) as exc:
|
|
685
893
|
print(f"执行失败:{exc}", file=sys.stderr)
|
|
686
894
|
return 1
|
|
895
|
+
finally:
|
|
896
|
+
cleanup_active_artifacts()
|
|
687
897
|
|
|
688
898
|
|
|
689
899
|
if __name__ == "__main__":
|