@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.
Files changed (98) hide show
  1. package/README.md +5 -0
  2. package/dist/commands/install.d.ts.map +1 -1
  3. package/dist/commands/install.js +13 -12
  4. package/dist/commands/install.js.map +1 -1
  5. package/dist/commands/uninstall.d.ts.map +1 -1
  6. package/dist/commands/uninstall.js +17 -9
  7. package/dist/commands/uninstall.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +5 -5
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/index.js +4 -4
  12. package/dist/index.js.map +1 -1
  13. package/dist/utils/config.d.ts +5 -3
  14. package/dist/utils/config.d.ts.map +1 -1
  15. package/dist/utils/config.js +34 -12
  16. package/dist/utils/config.js.map +1 -1
  17. package/dist/utils/file.d.ts +8 -0
  18. package/dist/utils/file.d.ts.map +1 -1
  19. package/dist/utils/file.js +161 -9
  20. package/dist/utils/file.js.map +1 -1
  21. package/dist/utils/jq.d.ts.map +1 -1
  22. package/dist/utils/jq.js +8 -0
  23. package/dist/utils/jq.js.map +1 -1
  24. package/package.json +6 -5
  25. package/skill/SKILL.md +15 -169
  26. package/skill/VERSION +1 -1
  27. package/skill/references/integration/flow.md +321 -292
  28. package/skill/references/integration/modules/alipay-sdk-reminder.md +5 -3
  29. package/skill/references/integration/modules/checklist.md +41 -27
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. package/skill/references/integration/modules/interface-contract/aipay-interface-contract.md +53 -0
  51. package/skill/references/integration/modules/interface-contract/apppay-interface-contract.md +45 -0
  52. package/skill/references/integration/modules/interface-contract/common-trade-contract.md +42 -0
  53. package/skill/references/integration/modules/interface-contract/notify-url-contract.md +68 -0
  54. package/skill/references/integration/modules/interface-contract/webpay-interface-contract.md +44 -0
  55. package/skill/references/integration/modules/interface-guide.md +37 -72
  56. package/skill/references/integration/modules/product-decision.md +10 -27
  57. package/skill/references/integration/modules/sandbox/a2m-sandbox-test.md +62 -94
  58. package/skill/references/integration/modules/sandbox/alipay-sandbox-tool.md +43 -40
  59. package/skill/references/integration/modules/sandbox/sandbox-setup-guide.md +93 -114
  60. package/skill/references/integration/modules/scripts/local_402_sandbox_pay.py +357 -147
  61. package/skill/references/integration/modules/scripts/sandbox_config.sh +299 -0
  62. package/skill/references/integration/modules/troubleshooting-flow.md +28 -0
  63. package/skill/references/normal/alipay-cli-env.md +15 -5
  64. package/skill/references/normal/full-process-routing.md +80 -0
  65. package/skill/references/normal/json/customer-messages.json +937 -0
  66. package/skill/references/normal/json/policy-rules.json +179 -0
  67. package/skill/references/normal/rejection-guide.md +11 -6
  68. package/skill/references/normal/scripts/alipay_cli_env.mjs +29 -0
  69. package/skill/references/normal/scripts/alipay_cli_env.sh +21 -0
  70. package/skill/references/normal/scripts/common.sh +87 -0
  71. package/skill/references/normal/scripts/detect_dev_tool.sh +5 -1
  72. package/skill/references/normal/scripts/integration_message_runner.mjs +801 -0
  73. package/skill/references/normal/scripts/onboarding_message_runner.mjs +436 -0
  74. package/skill/references/normal/scripts/open_official_url.sh +82 -0
  75. package/skill/references/normal/scripts/platform_compat.mjs +39 -0
  76. package/skill/references/normal/scripts/project_route_inspector.sh +132 -0
  77. package/skill/references/normal/scripts/python_script_runner.mjs +56 -0
  78. package/skill/references/normal/scripts/render_customer_message.mjs +1263 -0
  79. package/skill/references/normal/scripts/self_update.mjs +160 -0
  80. package/skill/references/normal/scripts/telemetry.mjs +1278 -0
  81. package/skill/references/normal/self-update.md +18 -51
  82. package/skill/references/onboarding/flow.md +381 -283
  83. package/skill/references/onboarding/modules/app-release.md +132 -179
  84. package/skill/references/onboarding/modules/authorization.md +53 -64
  85. package/skill/references/onboarding/modules/error-handling.md +31 -34
  86. package/skill/references/onboarding/modules/product-sign.md +62 -66
  87. package/skill/references/onboarding/modules/scripts/app.sh +397 -158
  88. package/skill/references/onboarding/modules/scripts/ar_sign_apply.sh +38 -22
  89. package/skill/references/onboarding/modules/scripts/auth.sh +479 -167
  90. package/skill/references/onboarding/modules/scripts/download_key_tool.sh +157 -0
  91. package/skill/references/onboarding/modules/scripts/error_handler.sh +100 -130
  92. package/skill/references/onboarding/modules/scripts/network_retry.sh +96 -0
  93. package/skill/references/onboarding/modules/scripts/query_sign_status.sh +17 -70
  94. package/skill/references/onboarding/modules/scripts/service.sh +153 -89
  95. package/skill/references/onboarding/modules/scripts/sign_status_parser.sh +58 -0
  96. package/skill/references/onboarding/modules/scripts/upload_screenshots.sh +16 -14
  97. package/skill/references/onboarding/modules/service-registration.md +67 -72
  98. package/skill/references/runtime-entry.md +62 -0
@@ -0,0 +1,157 @@
1
+ #!/bin/bash
2
+
3
+ set -u
4
+ umask 077
5
+
6
+ MANUAL_URL="https://opendocs.alipay.com/isv/02kipk"
7
+ DEST_DIR="${1:-}"
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ RENDERER="${SCRIPT_DIR}/../../../normal/scripts/render_customer_message.mjs"
10
+
11
+ manual_fallback_message() {
12
+ echo "未能完成自动下载,可能是当前环境不支持、无法访问下载源、本地目录不可写或安装包校验未通过。请访问支付宝开放平台手动下载支付宝开放平台密钥工具:"
13
+ echo
14
+ echo "$MANUAL_URL"
15
+ echo
16
+ echo "仅向 Agent 提供【应用公钥】,应用私钥请自行妥善保管。"
17
+ }
18
+
19
+ render_fallback() {
20
+ if command -v node >/dev/null 2>&1 && [ -f "$RENDERER" ]; then
21
+ printf '%s' '{}' | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=download_key_tool.sh node "$RENDERER" key_tool.download.fallback --variant DEFAULT && return 0
22
+ fi
23
+ manual_fallback_message
24
+ }
25
+
26
+ render_downloaded() {
27
+ local target_file="$1"
28
+ local message_input
29
+ if command -v node >/dev/null 2>&1 && [ -f "$RENDERER" ]; then
30
+ message_input=$(node -e 'process.stdout.write(JSON.stringify({downloadPath: process.argv[1]}))' "$target_file") \
31
+ && printf '%s' "$message_input" | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=download_key_tool.sh node "$RENDERER" key_tool.download.result --variant DOWNLOADED \
32
+ && return 0
33
+ fi
34
+ render_fallback
35
+ }
36
+
37
+ fail_download() {
38
+ render_fallback
39
+ exit 0
40
+ }
41
+
42
+ is_windows_reparse_point() {
43
+ local path="$1" powershell_rc
44
+ case "$(uname -s 2>/dev/null || true)" in MINGW*|MSYS*|CYGWIN*) ;; *) return 1 ;; esac
45
+ command -v powershell.exe >/dev/null 2>&1 || return 0
46
+ env "ALIPAY_AIPAY_REPARSE_PATH=$path" powershell.exe -NoProfile -NonInteractive -Command '
47
+ try {
48
+ $item = Get-Item -LiteralPath $env:ALIPAY_AIPAY_REPARSE_PATH -Force -ErrorAction Stop
49
+ } catch {
50
+ exit 2
51
+ }
52
+ if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { exit 0 } else { exit 1 }
53
+ ' >/dev/null 2>&1
54
+ powershell_rc=$?
55
+ [ "$powershell_rc" -eq 0 ] && return 0
56
+ [ "$powershell_rc" -eq 1 ] && return 1
57
+ return 0
58
+ }
59
+
60
+ OS_NAME="${ALIPAY_AIPAY_TEST_OS:-$(uname -s 2>/dev/null || true)}"
61
+ case "$OS_NAME" in
62
+ Darwin) PLATFORM="mac"; EXT="dmg" ;;
63
+ MINGW*|MSYS*|CYGWIN*|Windows_NT) PLATFORM="win"; EXT="exe" ;;
64
+ *) fail_download "UNSUPPORTED_OS" ;;
65
+ esac
66
+
67
+ if [ -z "$DEST_DIR" ]; then
68
+ DEST_DIR="${HOME:-}/Downloads"
69
+ fi
70
+ [ -n "$DEST_DIR" ] && [ -d "$DEST_DIR" ] && [ -w "$DEST_DIR" ] && [ ! -L "$DEST_DIR" ] || fail_download "DEST_DIR_UNAVAILABLE"
71
+ ! is_windows_reparse_point "$DEST_DIR" || fail_download "DEST_DIR_UNAVAILABLE"
72
+ command -v curl >/dev/null 2>&1 || fail_download "DEPENDENCY_MISSING"
73
+ command -v node >/dev/null 2>&1 || fail_download "DEPENDENCY_MISSING"
74
+
75
+ CURRENT_URL="https://ideservice.alipay.com/ide/getPluginUrl.htm?clientType=assistant&platform=${PLATFORM}&channelType=WEB"
76
+ PART_FILE=$(mktemp "${DEST_DIR}/.alipay-key-tool.XXXXXX.part") || fail_download "DEST_DIR_UNAVAILABLE"
77
+ HEADER_FILE=$(mktemp "${DEST_DIR}/.alipay-key-tool.XXXXXX.headers") || { rm -f "$PART_FILE"; fail_download "DEST_DIR_UNAVAILABLE"; }
78
+ chmod 600 "$PART_FILE" "$HEADER_FILE" 2>/dev/null || true
79
+ cleanup_download() { rm -f "$PART_FILE" "$HEADER_FILE"; }
80
+ trap cleanup_download EXIT INT TERM
81
+
82
+ REDIRECTS=0
83
+ while true; do
84
+ : > "$PART_FILE"
85
+ : > "$HEADER_FILE"
86
+ curl --silent --show-error --connect-timeout 10 --max-time 120 --dump-header "$HEADER_FILE" --output "$PART_FILE" --max-redirs 0 "$CURRENT_URL" 2>/dev/null
87
+ CURL_RC=$?
88
+ STATUS=$(awk '/^HTTP\// {code=$2; sub(/\r$/, "", code)} END {print code}' "$HEADER_FILE")
89
+ [ -n "$STATUS" ] || fail_download "NETWORK_UNAVAILABLE"
90
+ case "$STATUS" in
91
+ 301|302|303|307|308)
92
+ [ "$REDIRECTS" -lt 5 ] || fail_download "UNTRUSTED_REDIRECT"
93
+ LOCATION=$(awk 'tolower($0) ~ /^location:/ {sub(/^[^:]*:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' "$HEADER_FILE")
94
+ [ -n "$LOCATION" ] || fail_download "UNTRUSTED_REDIRECT"
95
+ if ! node -e '
96
+ const [raw] = process.argv.slice(1); let u;
97
+ try { u = new URL(raw); } catch { process.exit(1); }
98
+ if (u.protocol !== "https:" || u.username || u.password || u.port) process.exit(1);
99
+ if (!["ideservice.alipay.com", "mdn.alipayobjects.com"].includes(u.hostname)) process.exit(1);
100
+ ' "$LOCATION"; then fail_download "UNTRUSTED_REDIRECT"; fi
101
+ CURRENT_URL="$LOCATION"
102
+ REDIRECTS=$((REDIRECTS + 1))
103
+ ;;
104
+ 200)
105
+ [ "$CURL_RC" -eq 0 ] || fail_download "NETWORK_UNAVAILABLE"
106
+ FINAL_HOST=$(node -e 'console.log(new URL(process.argv[1]).hostname)' "$CURRENT_URL" 2>/dev/null) || fail_download "UNTRUSTED_REDIRECT"
107
+ [ "$FINAL_HOST" = "mdn.alipayobjects.com" ] || fail_download "UNTRUSTED_REDIRECT"
108
+ break
109
+ ;;
110
+ *) fail_download "NETWORK_UNAVAILABLE" ;;
111
+ esac
112
+ done
113
+
114
+ DISPOSITION=$(awk 'tolower($0) ~ /^content-disposition:/ {sub(/\r$/, ""); print; exit}' "$HEADER_FILE")
115
+ FILENAME=$(printf '%s' "$DISPOSITION" | sed -n 's/.*filename="\{0,1\}\([^";]*\)"\{0,1\}.*/\1/p')
116
+ if [ -z "$FILENAME" ]; then
117
+ FILENAME=$(node -e '
118
+ let url;
119
+ try { url = new URL(process.argv[1]); } catch { process.exit(1); }
120
+ process.stdout.write(url.searchParams.get("af_fileName") || "");
121
+ ' "$CURRENT_URL") || fail_download "INVALID_PACKAGE"
122
+ fi
123
+ case "$FILENAME" in ""|*/*|*\\*|.|..) fail_download "INVALID_PACKAGE" ;; esac
124
+ if ! node -e '
125
+ const name = process.argv[1] || "";
126
+ if (/[\u0000-\u001f\u007f`|]/.test(name)) process.exit(1);
127
+ ' "$FILENAME"; then fail_download "INVALID_PACKAGE"; fi
128
+ case "$FILENAME" in *.$EXT) ;; *) fail_download "INVALID_PACKAGE" ;; esac
129
+
130
+ SIZE=$(wc -c < "$PART_FILE" | tr -d ' ')
131
+ [ "$SIZE" -gt 0 ] || fail_download "INVALID_PACKAGE"
132
+ CONTENT_LENGTH=$(awk 'tolower($0) ~ /^content-length:/ {sub(/\r$/, ""); sub(/^[^:]*:[[:space:]]*/, ""); print; exit}' "$HEADER_FILE")
133
+ if [ -n "$CONTENT_LENGTH" ]; then
134
+ case "$CONTENT_LENGTH" in *[!0-9]*) fail_download "INVALID_PACKAGE" ;; esac
135
+ [ "$SIZE" -eq "$CONTENT_LENGTH" ] || fail_download "INVALID_PACKAGE"
136
+ fi
137
+ FIRST_NONSPACE=$(LC_ALL=C tr -d '[:space:]' < "$PART_FILE" | head -c 1)
138
+ case "$FIRST_NONSPACE" in '<'|'{'|'[') fail_download "INVALID_PACKAGE" ;; esac
139
+ if [ "$EXT" = "exe" ]; then
140
+ [ "$(head -c 2 "$PART_FILE")" = "MZ" ] || fail_download "INVALID_PACKAGE"
141
+ else
142
+ LC_ALL=C grep -a -q 'koly' "$PART_FILE" || fail_download "INVALID_PACKAGE"
143
+ fi
144
+
145
+ TARGET_FILE="${DEST_DIR}/${FILENAME}"
146
+ if [ -e "$TARGET_FILE" ] || [ -L "$TARGET_FILE" ]; then
147
+ BASE="${FILENAME%.*}"
148
+ TARGET_FILE="${DEST_DIR}/${BASE}-$(date +%Y%m%d%H%M%S)-$$.${EXT}"
149
+ fi
150
+ [ ! -e "$TARGET_FILE" ] && [ ! -L "$TARGET_FILE" ] || fail_download "DEST_DIR_UNAVAILABLE"
151
+ ln "$PART_FILE" "$TARGET_FILE" || fail_download "DEST_DIR_UNAVAILABLE"
152
+ rm -f "$PART_FILE"
153
+ trap - EXIT INT TERM
154
+ rm -f "$HEADER_FILE"
155
+ [ -f "$TARGET_FILE" ] && [ ! -L "$TARGET_FILE" ] || fail_download "DEST_DIR_UNAVAILABLE"
156
+ chmod 600 "$TARGET_FILE" 2>/dev/null || true
157
+ render_downloaded "$TARGET_FILE"
@@ -9,15 +9,22 @@
9
9
 
10
10
  ERROR_HANDLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
11
  COMMON_SCRIPT="${ERROR_HANDLER_DIR}/../../../normal/scripts/common.sh"
12
+ ERROR_RENDERER="${ERROR_HANDLER_DIR}/../../../normal/scripts/render_customer_message.mjs"
12
13
  if [ -f "$COMMON_SCRIPT" ]; then
13
14
  # shellcheck source=/dev/null
14
15
  source "$COMMON_SCRIPT"
15
- init_dev_tool_name
16
+ init_alipay_cli_context
16
17
  else
17
18
  echo "❌ 缺少公共脚本: $COMMON_SCRIPT"
18
19
  return 1 2>/dev/null || exit 1
19
20
  fi
20
21
 
22
+ render_error_message() {
23
+ local MESSAGE_ID="$1" VARIANT="$2" INPUT_JSON="$3"
24
+ printf '%s' "$INPUT_JSON" \
25
+ | ALIPAY_AIPAY_RENDERER_MANAGED_CALLER=error_handler.sh node "$ERROR_RENDERER" "$MESSAGE_ID" --variant "$VARIANT"
26
+ }
27
+
21
28
  # ─── MCP 信封解包 ──────────────────────────────────────────────────────────
22
29
  # alipay-cli mcp call 返回 MCP 协议信封:
23
30
  # { "content": [ { "text": "<业务JSON>", "type": "text" } ] }
@@ -104,6 +111,27 @@ extract_unique_json() {
104
111
  ' 2>/dev/null
105
112
  }
106
113
 
114
+ # 对客错误详情只保留受限单行文本。解析和错误分类仍使用原始值,
115
+ # 但任何疑似凭据、临时 URL 或密钥内容都不能进入普通终端输出。
116
+ sanitize_customer_error_text() {
117
+ local VALUE="${1:-}" NORMALIZED
118
+ NORMALIZED=$(printf '%s' "$VALUE" | tr '\000-\037\177' ' ' | awk '{$1=$1; print}')
119
+ if [ -z "$NORMALIZED" ]; then
120
+ echo "未知错误"
121
+ return
122
+ fi
123
+ if printf '%s' "$NORMALIZED" | grep -qiE '\{\{|\}\}|<INTERNAL_'; then
124
+ echo "错误详情包含内部标记或模板占位符,已隐藏"
125
+ return
126
+ fi
127
+ if printf '%s' "$NORMALIZED" | grep -qiE \
128
+ 'https?://|verification_url|device[_-]?code|authorization:[[:space:]]*bearer|payment-proof|-----BEGIN|private[_ -]?key|public[_ -]?key|["'\'']?(password|passwd|secret|api[_-]?key|access[_-]?token)["'\'']?[[:space:]]*[:=]|(^|[^[:alnum:]_])token[=:]'; then
129
+ echo "错误详情包含临时链接或敏感字段,已隐藏"
130
+ return
131
+ fi
132
+ printf '%s' "$NORMALIZED" | cut -c1-1000
133
+ }
134
+
107
135
  unwrap_mcp() {
108
136
  local RAW="$1"
109
137
  if [ -z "$RAW" ]; then
@@ -120,7 +148,6 @@ unwrap_mcp() {
120
148
  ;;
121
149
  ambiguous)
122
150
  echo "CLI 输出包含多个 JSON 候选,无法唯一解析"
123
- echo "$RAW"
124
151
  return
125
152
  ;;
126
153
  *)
@@ -308,7 +335,7 @@ detect_error() {
308
335
  # 先解包 MCP 信封,再进行错误检测
309
336
  local UNWRAPPED=$(unwrap_mcp "$CLI_RESULT")
310
337
 
311
- # 1. 优先检测 MCP 认证错误(HTTP 401)
338
+ # 1. 优先检测 MCP 认证错误
312
339
  if echo "$UNWRAPPED" | grep -qiE "HTTP 401|Authorization is empty|非法的认证信息"; then
313
340
  echo "MCP_AUTH_ERROR"
314
341
  return
@@ -333,9 +360,7 @@ detect_error() {
333
360
  fi
334
361
 
335
362
  if ! echo "$UNWRAPPED" | jq -e . >/dev/null 2>&1; then
336
- local FIRST_LINE
337
- FIRST_LINE=$(printf "%s" "$UNWRAPPED" | sed -n '1p')
338
- echo "CLI_ERROR:${FIRST_LINE:-CLI 返回非 JSON 内容}"
363
+ echo "CLI_ERROR:CLI 返回非 JSON 内容,原始输出已隐藏"
339
364
  return
340
365
  fi
341
366
 
@@ -361,44 +386,27 @@ detect_error() {
361
386
  # ─── MCP 认证错误处理 ──────────────────────────────────────────────────────
362
387
  handle_mcp_auth_error() {
363
388
  local LOGOUT_RESULT LOGOUT_EXIT_CODE LOGOUT_ERROR_TYPE LOGOUT_ERROR_MSG
389
+ local LOGOUT_ANALYSIS LOGOUT_PARSE_STATUS
364
390
 
365
- echo ""
366
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
367
- echo "🔐 未登录或授权已过期"
368
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
369
- echo ""
370
- echo "❌ 当前登录状态已失效,无法继续操作"
371
- echo ""
372
- echo "📋 原因:MCP 调用返回认证错误(HTTP 401)"
373
- echo "📋 说明:Authorization is empty(认证信息为空)"
374
- echo ""
375
- echo "🔄 正在退出当前登录..."
376
- LOGOUT_RESULT=$(PLATFORM=${DEV_TOOL_NAME:-unknown} alipay-cli logout --json 2>&1)
391
+ render_error_message error.mcp.auth START '{}' || return 1
392
+ LOGOUT_RESULT=$(PLATFORM=${DEV_TOOL_NAME:-unknown} PLATFORM_ID="${PLATFORM_ID:-}" alipay-cli logout --json 2>&1)
377
393
  LOGOUT_EXIT_CODE=$?
378
394
  LOGOUT_ERROR_TYPE=$(detect_error "$LOGOUT_RESULT")
395
+ LOGOUT_ANALYSIS=$(extract_unique_json "$LOGOUT_RESULT" 2>/dev/null || echo '{"status":"none"}')
396
+ LOGOUT_PARSE_STATUS=$(echo "$LOGOUT_ANALYSIS" | jq -r '.status // "none"' 2>/dev/null)
379
397
 
380
398
  if [ "$LOGOUT_EXIT_CODE" -ne 0 ] || [ "$LOGOUT_ERROR_TYPE" != "SUCCESS" ]; then
381
- echo ""
382
- echo "❌ 退出当前登录失败,已停止重新授权"
383
- if echo "$LOGOUT_RESULT" | jq -e . >/dev/null 2>&1; then
384
- LOGOUT_ERROR_MSG=$(extract_error_message "$LOGOUT_RESULT")
385
- echo "📋 错误信息:$LOGOUT_ERROR_MSG"
386
- elif [ -n "$LOGOUT_RESULT" ]; then
387
- echo "📋 错误信息:$LOGOUT_RESULT"
399
+ if [ "$LOGOUT_PARSE_STATUS" != "ok" ] || [ "$LOGOUT_ERROR_TYPE" = "MCP_SERVICE_ERROR" ] || [ "$LOGOUT_ERROR_TYPE" = "SERVICE_UNSTABLE" ]; then
400
+ render_error_message error.mcp.auth LOGOUT_UNCONFIRMED '{}' || return 1
401
+ else
402
+ LOGOUT_ERROR_MSG=$(sanitize_customer_error_text "$(extract_error_message "$LOGOUT_RESULT")")
403
+ MESSAGE_INPUT=$(jq -cn --arg errorMessage "$LOGOUT_ERROR_MSG" '{errorMessage:$errorMessage}')
404
+ render_error_message error.mcp.auth LOGOUT_FAILED "$MESSAGE_INPUT" || return 1
388
405
  fi
389
- echo "📋 请先重试退出登录;确认退出成功后再重新授权。"
390
406
  return 1
391
407
  fi
392
408
 
393
- echo ""
394
- echo "✅ 已退出登录状态"
395
- echo ""
396
- echo "📌 请重新执行登录授权流程:"
397
- echo " 1. 我会生成授权链接"
398
- echo " 2. 您使用支付宝扫码授权"
399
- echo " 3. 授权成功后继续当前操作"
400
- echo ""
401
- echo "📌 产品、MCC 和授权范围未变化时,沿用已有确认立即重新生成授权链接;发生变化时先重新确认受影响内容。"
409
+ render_error_message error.mcp.auth LOGOUT_SUCCESS '{}' || return 1
402
410
  }
403
411
 
404
412
  # ─── MCP 服务不可用处理 ────────────────────────────────────────────────────
@@ -406,57 +414,27 @@ handle_mcp_service_error() {
406
414
  local CLI_RESULT="$1"
407
415
  local UNWRAPPED=$(unwrap_mcp "$CLI_RESULT")
408
416
 
409
- echo ""
410
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
411
- echo "⚠️ MCP 服务暂时不可用"
412
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
413
- echo ""
414
417
  if [ "${DEV_TOOL_NAME:-unknown}" != "unknown" ]; then
415
- echo "❌ 当前命令需要访问支付宝服务,但当前 Agent 执行环境可能没有外网权限"
416
- echo ""
417
- echo "📋 处理方式:"
418
- echo " - Agent 应申请可联网权限后重试同一条命令"
419
- echo " - 不要要求用户排查本机网络,也不要更换业务参数"
420
- echo ""
421
- echo "请授权 Agent 联网重试,或稍后重新执行当前命令。"
418
+ render_error_message error.mcp.service AGENT_NETWORK '{}'
422
419
  return
423
420
  fi
424
421
 
425
- echo "❌ 无法连接到支付宝服务,请稍后重试"
426
- echo ""
427
- echo "📋 错误详情:"
428
- echo "$UNWRAPPED" | jq -r '.error.message // .errorMessage // .message // "未知错误"' 2>/dev/null || echo "$UNWRAPPED"
429
- echo ""
430
- echo "您可以:"
431
- echo " 1. 检查网络连接是否正常"
432
- echo " 2. 等待几分钟后重新执行"
433
- echo " 3. 联系技术支持获取帮助"
434
- echo ""
435
- echo "请回复「重试」重新尝试,或「退出」结束流程。"
422
+ local SERVICE_ERROR_MSG
423
+ SERVICE_ERROR_MSG=$(echo "$UNWRAPPED" | jq -r '.error.message // .errorMessage // .message // "未知错误"' 2>/dev/null) || SERVICE_ERROR_MSG="服务调用失败,原始输出已隐藏"
424
+ SERVICE_ERROR_MSG=$(sanitize_customer_error_text "$SERVICE_ERROR_MSG")
425
+ MESSAGE_INPUT=$(jq -cn --arg errorDetail "$SERVICE_ERROR_MSG" '{errorDetail:$errorDetail}')
426
+ render_error_message error.mcp.service GENERIC "$MESSAGE_INPUT"
436
427
  }
437
428
 
438
429
  # ─── 授权信息不匹配处理 ────────────────────────────────────────────────────
439
430
  handle_auth_mismatch() {
440
- local CLI_RESULT="$1"
441
- local UNWRAPPED=$(unwrap_mcp "$CLI_RESULT")
442
-
443
- echo ""
444
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
445
- echo "❌ 授权信息不匹配,需要重新授权"
446
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
447
-
448
- if echo "$UNWRAPPED" | grep -qi "mccCode is not auth"; then
449
- echo "📋 原因:经营类目未授权"
450
- elif echo "$UNWRAPPED" | grep -qi "salesProductCodes is not auth"; then
451
- echo "📋 原因:产品未授权"
452
- elif echo "$UNWRAPPED" | grep -qi "scope is not auth"; then
453
- echo "📋 原因:授权 scope 不满足"
431
+ local UNWRAPPED
432
+ UNWRAPPED=$(unwrap_mcp "${1:-}")
433
+ if echo "$UNWRAPPED" | grep -qiE "mccCode.*is not auth"; then
434
+ emit_control "AUTH_FLOW:MCC_MISMATCH"
435
+ else
436
+ emit_control "AUTH_FLOW:SCOPE_MISMATCH"
454
437
  fi
455
-
456
- echo ""
457
- echo ""
458
- echo "⛔ 已停止当前操作和后续调用"
459
- echo "📋 请调用 auth.sh mismatch;该命令会统一退出当前登录并使用正确范围重新授权"
460
438
  }
461
439
 
462
440
  # ─── 后端业务错误处理(完整透出错误信息) ──────────────────────────────────
@@ -465,75 +443,57 @@ handle_backend_error() {
465
443
  local ERROR_CODE="$2"
466
444
  local UNWRAPPED=$(unwrap_mcp "$CLI_RESULT")
467
445
 
468
- echo ""
469
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
470
- echo "❌ 业务处理失败"
471
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
472
- echo ""
473
- echo "📋 错误码:$ERROR_CODE"
474
-
475
446
  # 提取错误信息
476
447
  local ERROR_MSG=$(extract_error_message "$UNWRAPPED")
477
448
  local BIZ_TIPS=$(extract_biz_tips "$UNWRAPPED")
478
- local NEED_RETRY=$(extract_need_retry "$UNWRAPPED")
479
449
  local ERROR_SCENE=$(extract_error_object_field "$UNWRAPPED" "errorScene")
480
450
  local ERROR_SPECIFIC=$(extract_error_object_field "$UNWRAPPED" "errorSpecific")
481
451
 
482
- echo "📋 错误信息:$ERROR_MSG"
483
-
484
- if [ -n "$ERROR_SCENE" ] || [ -n "$ERROR_SPECIFIC" ]; then
485
- echo "📋 错误场景:${ERROR_SCENE:-未知} / ${ERROR_SPECIFIC:-未知}"
486
- fi
452
+ ERROR_MSG=$(sanitize_customer_error_text "$ERROR_MSG")
453
+ if [ -n "$ERROR_SCENE" ]; then ERROR_SCENE=$(sanitize_customer_error_text "$ERROR_SCENE"); fi
454
+ if [ -n "$ERROR_SPECIFIC" ]; then ERROR_SPECIFIC=$(sanitize_customer_error_text "$ERROR_SPECIFIC"); fi
487
455
 
488
456
  # 透出 checkedError 中的详细错误描述(如 ar-sign.apply 返回的字段级校验错误)
489
457
  local CHECKED_ERRORS=$(extract_checked_errors "$UNWRAPPED")
458
+ local CHECKED_ERRORS_JSON='[]'
490
459
  if [ -n "$CHECKED_ERRORS" ] && [ "$CHECKED_ERRORS" != "null" ] && [ "$CHECKED_ERRORS" != "[]" ]; then
491
- echo ""
492
- echo "📋 详细校验错误:"
493
- echo "$CHECKED_ERRORS" | jq -r '.[]? | .errorDesc // .message // . // empty' 2>/dev/null | while IFS= read -r line; do
494
- [ -n "$line" ] && echo " - $line"
495
- done
460
+ while IFS= read -r line; do
461
+ if [ -n "$line" ]; then
462
+ line=$(sanitize_customer_error_text "$line")
463
+ CHECKED_ERRORS_JSON=$(jq -cn --argjson items "$CHECKED_ERRORS_JSON" --arg item "$line" '$items + [$item]')
464
+ fi
465
+ done < <(echo "$CHECKED_ERRORS" | jq -r '.[]? | .errorDesc // .message // . // empty' 2>/dev/null)
496
466
  fi
497
467
 
498
- if [ -n "$BIZ_TIPS" ] && [ "$BIZ_TIPS" != "null" ] && [ "$BIZ_TIPS" != "" ]; then
499
- echo ""
500
- echo "💡 提示:$BIZ_TIPS"
468
+ if [ -n "$BIZ_TIPS" ] && [ "$BIZ_TIPS" != "null" ]; then
469
+ BIZ_TIPS=$(sanitize_customer_error_text "$BIZ_TIPS")
470
+ else
471
+ BIZ_TIPS=""
501
472
  fi
502
-
473
+ local APP_MAX="NO"
503
474
  if [ "$ERROR_CODE" = "APP_MAX_ERROR" ]; then
504
- echo ""
505
- echo "📌 处理建议:当前主体下应用数量已达到上限,无法继续新建应用。请优先复用已有上线应用;如必须新建,请前往支付宝开放平台处理应用配额或联系支付宝技术支持。"
506
- fi
507
-
508
- if [ "$NEED_RETRY" = "true" ]; then
509
- echo ""
510
- echo "🔄 此错误可重试,请回复「重试」重新执行"
475
+ APP_MAX="YES"
511
476
  fi
512
-
513
- echo ""
514
- echo "如需帮助,请联系技术支持并提供以上错误信息。"
477
+ MESSAGE_INPUT=$(jq -cn \
478
+ --arg errorCode "$(sanitize_customer_error_text "$ERROR_CODE")" \
479
+ --arg errorMessage "$ERROR_MSG" \
480
+ --arg errorScene "$ERROR_SCENE" \
481
+ --arg errorSpecific "$ERROR_SPECIFIC" \
482
+ --argjson checkedErrors "$CHECKED_ERRORS_JSON" \
483
+ --arg bizTips "$BIZ_TIPS" \
484
+ --arg appMax "$APP_MAX" \
485
+ '{errorCode:$errorCode,errorMessage:$errorMessage,errorScene:$errorScene,errorSpecific:$errorSpecific,checkedErrors:$checkedErrors,bizTips:$bizTips,appMax:$appMax}')
486
+ render_error_message error.backend DEFAULT "$MESSAGE_INPUT"
515
487
  }
516
488
 
517
489
  # ─── MCP 服务不稳定处理 ────────────────────────────────────────────────────
518
490
  handle_service_unstable() {
519
- echo ""
520
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
521
- echo "⚠️ MCP 服务暂时不稳定"
522
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
523
- echo ""
524
- echo "当前无法继续执行,请稍后重试。"
525
- echo ""
526
- echo "您可以:"
527
- echo " 1. 等待几分钟后重新执行"
528
- echo " 2. 检查网络连接是否正常"
529
- echo " 3. 联系技术支持获取帮助"
530
- echo ""
531
- echo "请回复「重试」重新尝试,或「退出」结束流程。"
491
+ render_error_message error.service.unstable DEFAULT '{}'
532
492
  }
533
493
 
534
494
  # ─── 统一错误处理入口 ──────────────────────────────────────────────────────
535
495
  # 参数: $1 - CLI 执行结果文本(原始输出,含信封或不含)
536
- # 返回: 0=成功, 1=失败(需要用户干预)
496
+ # 返回: 0=成功, 1=当前动作失败(由主流程判断恢复、阻断或继续独立分支)
537
497
  handle_error() {
538
498
  local CLI_RESULT="$1"
539
499
  local ERROR_TYPE=$(detect_error "$CLI_RESULT")
@@ -559,19 +519,29 @@ handle_error() {
559
519
  ;;
560
520
  "CLI_ERROR:"*)
561
521
  local MSG="${ERROR_TYPE#CLI_ERROR:}"
562
- echo "❌ 命令执行失败:$MSG"
563
522
  # CLI_ERROR 也透出 bizTips 和 checkedError(如果有的话)
564
523
  local CLI_BIZ_TIPS=$(extract_biz_tips "$_UNWRAPPED")
565
- if [ -n "$CLI_BIZ_TIPS" ] && [ "$CLI_BIZ_TIPS" != "null" ] && [ "$CLI_BIZ_TIPS" != "" ]; then
566
- echo "💡 $CLI_BIZ_TIPS"
524
+ if [ -n "$CLI_BIZ_TIPS" ] && [ "$CLI_BIZ_TIPS" != "null" ]; then
525
+ CLI_BIZ_TIPS=$(sanitize_customer_error_text "$CLI_BIZ_TIPS")
526
+ else
527
+ CLI_BIZ_TIPS=""
567
528
  fi
568
529
  local CLI_CHECKED=$(extract_checked_errors "$_UNWRAPPED")
530
+ local CLI_CHECKED_JSON='[]'
569
531
  if [ -n "$CLI_CHECKED" ] && [ "$CLI_CHECKED" != "null" ] && [ "$CLI_CHECKED" != "[]" ]; then
570
- echo "📋 详细校验错误:"
571
- echo "$CLI_CHECKED" | jq -r '.[]? | .errorDesc // .message // . // empty' 2>/dev/null | while IFS= read -r line; do
572
- [ -n "$line" ] && echo " - $line"
573
- done
532
+ while IFS= read -r line; do
533
+ if [ -n "$line" ]; then
534
+ line=$(sanitize_customer_error_text "$line")
535
+ CLI_CHECKED_JSON=$(jq -cn --argjson items "$CLI_CHECKED_JSON" --arg item "$line" '$items + [$item]')
536
+ fi
537
+ done < <(echo "$CLI_CHECKED" | jq -r '.[]? | .errorDesc // .message // . // empty' 2>/dev/null)
574
538
  fi
539
+ MESSAGE_INPUT=$(jq -cn \
540
+ --arg errorMessage "$(sanitize_customer_error_text "$MSG")" \
541
+ --arg bizTips "$CLI_BIZ_TIPS" \
542
+ --argjson checkedErrors "$CLI_CHECKED_JSON" \
543
+ '{errorMessage:$errorMessage,bizTips:$bizTips,checkedErrors:$checkedErrors}')
544
+ render_error_message error.cli DEFAULT "$MESSAGE_INPUT"
575
545
  ;;
576
546
  "SUCCESS")
577
547
  return 0
@@ -0,0 +1,96 @@
1
+ #!/bin/bash
2
+
3
+ # Source after error_handler.sh. This wrapper never changes MCP argv or business JSON.
4
+
5
+ NETWORK_RETRY_CLASS=""
6
+ NETWORK_DISPATCH_STATE=""
7
+ NETWORK_ATTEMPTS=0
8
+
9
+ network_retry_is_local_transport_error() {
10
+ printf '%s' "$1" | grep -Eqi \
11
+ 'could not resolve host|name or service not known|nodename nor servname|connection refused|failed to connect|network access[^[:cntrl:]]*(denied|blocked)[^[:cntrl:]]*before connection'
12
+ }
13
+
14
+ network_retry_is_not_sent() {
15
+ printf '%s' "$1" | grep -Eqi \
16
+ 'could not resolve host|name or service not known|nodename nor servname|connection refused|network access[^[:cntrl:]]*(denied|blocked)[^[:cntrl:]]*before connection'
17
+ }
18
+
19
+ run_network_retry() {
20
+ local result_var="$1" operation="$2" action_name="$3"
21
+ shift 3
22
+ [ "${1:-}" = "--" ] || { echo "NETWORK_RETRY_ERROR:缺少 -- 参数分隔符" >&2; return 64; }
23
+ shift
24
+ [ "$#" -gt 0 ] || { echo "NETWORK_RETRY_ERROR:缺少命令 argv" >&2; return 64; }
25
+ case "$operation" in read|write) ;; *) echo "NETWORK_RETRY_ERROR:operation 必须为 read 或 write" >&2; return 64 ;; esac
26
+
27
+ local max_retries="${NETWORK_RETRY_MAX_RETRIES:-2}"
28
+ local delay_seconds="${NETWORK_RETRY_DELAY_SECONDS:-3}"
29
+ local attempt=0 stdout_file stderr_file stdout stderr rc classification transient dispatch
30
+ stdout_file=$(mktemp "${TMPDIR:-/tmp}/alipay-aipay-stdout.XXXXXX") || return 1
31
+ stderr_file=$(mktemp "${TMPDIR:-/tmp}/alipay-aipay-stderr.XXXXXX") || { rm -f "$stdout_file"; return 1; }
32
+ chmod 600 "$stdout_file" "$stderr_file" 2>/dev/null || true
33
+ trap 'rm -f "$stdout_file" "$stderr_file"; trap - RETURN' RETURN
34
+
35
+ while true; do
36
+ attempt=$((attempt + 1))
37
+ : > "$stdout_file"
38
+ : > "$stderr_file"
39
+ "$@" >"$stdout_file" 2>"$stderr_file"
40
+ rc=$?
41
+ stdout=$(cat "$stdout_file")
42
+ stderr=$(cat "$stderr_file")
43
+ classification=""
44
+ transient=false
45
+ dispatch="MAYBE_SENT"
46
+
47
+ local stdout_is_business_json=false unwrapped_stdout=""
48
+ if [ -n "$stdout" ]; then
49
+ classification=$(detect_error "$stdout" 2>/dev/null || true)
50
+ unwrapped_stdout=$(unwrap_mcp "$stdout" 2>/dev/null || true)
51
+ if printf '%s' "$unwrapped_stdout" | jq -e . >/dev/null 2>&1; then
52
+ stdout_is_business_json=true
53
+ fi
54
+ # 合法业务 JSON 始终以 stdout 为准。普通启动日志会被 detect_error 归为
55
+ # CLI_ERROR;此时仍须检查 stderr,避免真实连接失败被非空 stdout 掩盖。
56
+ if [ "$stdout_is_business_json" != true ] && [ "$rc" -ne 0 ] && [ -n "$stderr" ]; then
57
+ classification=$(detect_error "$stderr" 2>/dev/null || true)
58
+ fi
59
+ elif [ -n "$stderr" ]; then
60
+ classification=$(detect_error "$stderr" 2>/dev/null || true)
61
+ fi
62
+
63
+ case "$classification" in
64
+ MCP_SERVICE_ERROR|SERVICE_UNSTABLE) transient=true ;;
65
+ *)
66
+ if [ "$stdout_is_business_json" != true ] && { [ "$rc" -ne 0 ] || [ -n "$stderr" ]; } && network_retry_is_local_transport_error "$stderr"; then
67
+ classification="LOCAL_TRANSPORT_ERROR"
68
+ transient=true
69
+ fi
70
+ ;;
71
+ esac
72
+
73
+ if network_retry_is_not_sent "$stderr"; then
74
+ dispatch="NOT_SENT"
75
+ fi
76
+
77
+ NETWORK_RETRY_CLASS="$classification"
78
+ NETWORK_DISPATCH_STATE="$dispatch"
79
+ NETWORK_ATTEMPTS="$attempt"
80
+ printf -v "$result_var" '%s' "$stdout"
81
+
82
+ if [ "$transient" != true ]; then
83
+ return 0
84
+ fi
85
+
86
+ if [ "$operation" = "write" ] && [ "$dispatch" != "NOT_SENT" ]; then
87
+ return 75
88
+ fi
89
+
90
+ if [ "$attempt" -gt "$max_retries" ]; then
91
+ echo "NETWORK_RETRY_EXHAUSTED:${action_name}:${classification}:${dispatch}:${attempt}" >&2
92
+ return 76
93
+ fi
94
+ sleep "$delay_seconds"
95
+ done
96
+ }