@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.
- package/bin/alipay-aipay.js +6 -0
- package/dist/commands/install.d.ts +11 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +182 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/prepare-tools.d.ts +5 -0
- package/dist/commands/prepare-tools.d.ts.map +1 -0
- package/dist/commands/prepare-tools.js +29 -0
- package/dist/commands/prepare-tools.js.map +1 -0
- package/dist/commands/uninstall.d.ts +10 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +82 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +155 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/alipay-cli.d.ts +15 -0
- package/dist/utils/alipay-cli.d.ts.map +1 -0
- package/dist/utils/alipay-cli.js +221 -0
- package/dist/utils/alipay-cli.js.map +1 -0
- package/dist/utils/config.d.ts +48 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +207 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/file.d.ts +55 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +154 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/gpg.d.ts +14 -0
- package/dist/utils/gpg.d.ts.map +1 -0
- package/dist/utils/gpg.js +247 -0
- package/dist/utils/gpg.js.map +1 -0
- package/dist/utils/jq.d.ts +14 -0
- package/dist/utils/jq.d.ts.map +1 -0
- package/dist/utils/jq.js +221 -0
- package/dist/utils/jq.js.map +1 -0
- package/dist/utils/npm.d.ts +17 -0
- package/dist/utils/npm.d.ts.map +1 -0
- package/dist/utils/npm.js +42 -0
- package/dist/utils/npm.js.map +1 -0
- package/dist/utils/shell.d.ts +11 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +54 -0
- package/dist/utils/shell.js.map +1 -0
- package/package.json +48 -7
- package/skill/SKILL.md +118 -0
- package/skill/references/integration/flow.md +487 -0
- package/skill/references/integration/modules/alipay-sdk-reminder.md +256 -0
- package/skill/references/integration/modules/checklist.md +122 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/skill/references/integration/modules/interface-guide.md +90 -0
- package/skill/references/integration/modules/product-decision.md +90 -0
- package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +270 -0
- package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +133 -0
- package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +218 -0
- package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +659 -0
- package/skill/references/normal/alipay-cli-env.md +214 -0
- package/skill/references/normal/rejection-guide.md +37 -0
- package/skill/references/normal/scripts/common.sh +32 -0
- package/skill/references/normal/scripts/detect_dev_tool.sh +46 -0
- package/skill/references/onboarding/flow.md +573 -0
- package/skill/references/onboarding/modules/app-release.md +415 -0
- package/skill/references/onboarding/modules/authorization.md +269 -0
- package/skill/references/onboarding/modules/error-handling.md +251 -0
- package/skill/references/onboarding/modules/mcc-reference.md +209 -0
- package/skill/references/onboarding/modules/product-sign.md +304 -0
- package/skill/references/onboarding/modules/scripts/app.sh +705 -0
- package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +248 -0
- package/skill/references/onboarding/modules/scripts/auth.sh +505 -0
- package/skill/references/onboarding/modules/scripts/error_handler.sh +452 -0
- package/skill/references/onboarding/modules/scripts/query_sign_status.sh +146 -0
- package/skill/references/onboarding/modules/scripts/service.sh +267 -0
- package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +152 -0
- 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:]))
|